"""Tests for the live MCP JSON-RPC transport in abom.remote.fetch_mcp.

Exercised against an httpx.MockTransport that simulates a live MCP server in
both application/json and text/event-stream framings, so the whole fetch_mcp
path is covered with no network. This is the transport the broker reuses, so it
must be trustworthy before we build on it.

The pure helpers (URL predicates, mcp_to_scan_result) are covered in
test_remote.py; this file covers only the transport that was previously untested.
"""
import json

import pytest

# The MCP transport needs httpx (the optional [mcp] extra). Skip the whole
# module cleanly if it isn't installed, rather than erroring at collection.
httpx = pytest.importorskip("httpx")

from abom import remote


def _mcp_handler(*, sse=False, error_on=None, init_status=200):
    """An httpx MockTransport handler that answers MCP JSON-RPC calls."""
    results = {
        "initialize": {"serverInfo": {"name": "demo-mcp", "version": "2.1"}},
        "tools/list": {"tools": [{"name": "search", "description": "web search"},
                                  {"name": "fetch"}]},
        "resources/list": {"resources": [{"name": "kb", "uri": "res://kb"}]},
        "prompts/list": {"prompts": [{"name": "system"}]},
    }

    def handler(request: httpx.Request) -> httpx.Response:
        body = json.loads(request.content)
        method = body.get("method")
        if method == "initialize" and init_status >= 400:
            return httpx.Response(init_status)
        if method == "notifications/initialized":
            return httpx.Response(202)
        if error_on and method == error_on:
            payload = {"jsonrpc": "2.0", "id": body.get("id"),
                       "error": {"code": -32601, "message": "boom"}}
        else:
            payload = {"jsonrpc": "2.0", "id": body.get("id"),
                       "result": results.get(method, {})}
        headers = {"mcp-session-id": "sess-123"}
        if sse:
            text = f"event: message\ndata: {json.dumps(payload)}\n\n"
            return httpx.Response(200, text=text,
                                  headers={**headers, "content-type": "text/event-stream"})
        return httpx.Response(200, json=payload, headers=headers)

    return handler


@pytest.fixture
def patch_client(monkeypatch):
    """Make remote.fetch_mcp build a MockTransport-backed httpx.Client."""
    def _install(handler):
        transport = httpx.MockTransport(handler)
        real_client = httpx.Client

        def factory(*args, **kwargs):
            kwargs["transport"] = transport
            return real_client(*args, **kwargs)

        monkeypatch.setattr(httpx, "Client", factory)
    return _install


def test_fetch_mcp_json_enumerates_capabilities(patch_client):
    patch_client(_mcp_handler(sse=False))
    caps = remote.fetch_mcp("https://mcp.example.com/rpc")
    assert caps["server"] == {"name": "demo-mcp", "version": "2.1"}
    assert {t["name"] for t in caps["tools"]} == {"search", "fetch"}
    assert caps["resources"][0]["uri"] == "res://kb"
    assert caps["prompts"][0]["name"] == "system"


def test_fetch_mcp_sse_framing(patch_client):
    """Server answers as an SSE stream; the last data: line is parsed."""
    patch_client(_mcp_handler(sse=True))
    caps = remote.fetch_mcp("https://mcp.example.com/rpc")
    assert caps["server"]["name"] == "demo-mcp"
    assert {t["name"] for t in caps["tools"]} == {"search", "fetch"}


def test_fetch_mcp_initialize_http_error(patch_client):
    patch_client(_mcp_handler(init_status=401))
    with pytest.raises(RuntimeError, match="initialize failed: HTTP 401"):
        remote.fetch_mcp("https://mcp.example.com/rpc")


def test_fetch_mcp_initialize_jsonrpc_error(patch_client):
    patch_client(_mcp_handler(error_on="initialize"))
    with pytest.raises(RuntimeError, match="initialize error: boom"):
        remote.fetch_mcp("https://mcp.example.com/rpc")


def test_fetch_mcp_tolerates_list_method_error(patch_client):
    """A server that errors on tools/list still yields (empty) rather than crashing."""
    patch_client(_mcp_handler(error_on="tools/list"))
    caps = remote.fetch_mcp("https://mcp.example.com/rpc")
    assert caps["tools"] == []
    assert caps["resources"]  # the other lists still work


def test_fetch_mcp_passes_auth_header(patch_client):
    seen = {}

    def handler(request):
        seen["auth"] = request.headers.get("authorization")
        payload = {"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "x"}}}
        return httpx.Response(200, json=payload)

    patch_client(handler)
    remote.fetch_mcp("https://mcp.example.com/rpc", headers={"Authorization": "Bearer T"})
    assert seen["auth"] == "Bearer T"


def test_fetch_mcp_transport_failure_wrapped(patch_client):
    def handler(request):
        raise httpx.ConnectError("no route")

    patch_client(handler)
    with pytest.raises(RuntimeError, match="could not reach MCP endpoint"):
        remote.fetch_mcp("https://mcp.example.com/rpc")


def test_parse_empty_sse_stream_raises():
    resp = httpx.Response(200, text="event: ping\n\n",
                          headers={"content-type": "text/event-stream"})
    with pytest.raises(RuntimeError, match="empty SSE stream"):
        remote._parse_jsonrpc_body(resp)


# --------------------------------------------------------------------------
# fetch_git — clone a local file:// repo (no network), verify cleanup + errors
# --------------------------------------------------------------------------
def _make_local_repo(tmp_path):
    import subprocess
    repo = tmp_path / "origin"
    repo.mkdir()
    env = {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
           "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
    import os
    env = {**os.environ, **env}
    subprocess.run(["git", "init", "-q"], cwd=repo, check=True, env=env)
    (repo / "agent.py").write_text("MODEL = 'gpt-4o'\n")
    subprocess.run(["git", "add", "."], cwd=repo, check=True, env=env)
    subprocess.run(["git", "commit", "-qm", "init"], cwd=repo, check=True, env=env)
    return repo


def test_fetch_git_clones_and_cleans_up(tmp_path):
    repo = _make_local_repo(tmp_path)
    cloned_path = None
    with remote.fetch_git(f"file://{repo}") as dest:
        cloned_path = dest
        assert (dest / "agent.py").read_text().startswith("MODEL")
    # temp dir is removed on context exit
    assert not cloned_path.exists()


def test_fetch_git_bad_url_raises(tmp_path):
    with pytest.raises(RuntimeError, match="git clone failed"):
        with remote.fetch_git(f"file://{tmp_path}/does-not-exist"):
            pass


def test_fetch_git_missing_git_binary(monkeypatch):
    monkeypatch.setattr(remote.shutil, "which", lambda _: None)
    with pytest.raises(RuntimeError, match="git is not installed"):
        with remote.fetch_git("https://github.com/org/agent"):
            pass
