"""End-to-end test: MCP server dies mid-session.

Starts a real HTTP MCP server, connects MCPManager to it, stops
the server, and verifies the process survives.
"""

from __future__ import annotations

import asyncio
import json
import logging
import socket
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler

import pytest

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Minimal MCP server (just enough to connect + list tools + call tool)
# ---------------------------------------------------------------------------

class MCPHandler(BaseHTTPRequestHandler):
    """Bare-minimum MCP server over HTTP."""

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(length)) if length else {}
        method = body.get("method", "")
        req_id = body.get("id")

        if method == "initialize":
            result = {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {"listChanged": True}},
                "serverInfo": {"name": "test-mcp", "version": "1.0"},
            }
        elif method == "notifications/initialized":
            # No response needed for notifications
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(b"{}")
            return
        elif method == "tools/list":
            result = {
                "tools": [{
                    "name": "test_tool",
                    "description": "A test tool",
                    "inputSchema": {"type": "object", "properties": {}},
                }]
            }
        elif method == "tools/call":
            result = {
                "content": [{"type": "text", "text": "tool result"}],
                "isError": False,
            }
        else:
            result = {}

        response = json.dumps({"jsonrpc": "2.0", "id": req_id, "result": result})
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(response)))
        self.end_headers()
        self.wfile.write(response.encode())

    def log_message(self, format, *args):
        pass  # suppress request logs


def _find_free_port() -> int:
    with socket.socket() as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


class ManagedMCPServer:
    """Start/stop a test MCP server in a background thread."""

    def __init__(self):
        self.port = _find_free_port()
        self.url = f"http://127.0.0.1:{self.port}/mcp"
        self._server = HTTPServer(("127.0.0.1", self.port), MCPHandler)
        self._thread = None

    def start(self):
        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
        self._thread.start()

    def stop(self):
        self._server.shutdown()
        self._thread.join(timeout=5)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestMCPServerDeath:
    """Verify MCPManager and tool proxy survive a dead server."""

    @pytest.mark.asyncio
    async def test_tool_call_after_server_dies(self):
        """Tool proxy should return error string, not crash."""
        from icarus.mcp.client import MCPManager

        server = ManagedMCPServer()
        server.start()

        failures = []
        mgr = MCPManager(
            {"test": {"url": server.url}},
            on_failure=lambda name, msg: failures.append((name, msg)),
        )

        try:
            tools, failed = await mgr.start()
            assert failed == [], f"Connection failed: {failed}"
            assert len(tools) >= 1
            assert mgr.server_status["test"] == "connected"

            # Tool works while server is up
            tool = tools[0]
            result = await tool.ainvoke({})
            assert "tool result" in str(result)

            # Kill the server
            server.stop()

            # Tool call should return error, not crash
            result = await tool.ainvoke({})
            result_str = str(result)
            assert "[Tool unavailable]" in result_str or "error" in result_str.lower(), \
                f"Expected error message, got: {result_str}"

            # on_failure should have been called
            assert len(failures) >= 1

        finally:
            await mgr.stop()

    @pytest.mark.asyncio
    async def test_event_loop_survives_after_server_death(self):
        """The event loop should keep working after MCP server dies."""
        from icarus.mcp.client import MCPManager

        server = ManagedMCPServer()
        server.start()

        mgr = MCPManager(
            {"test": {"url": server.url}},
        )

        try:
            tools, failed = await mgr.start()
            assert failed == []

            # Kill the server
            server.stop()

            # Try a tool call (may fail/timeout — that's OK)
            tool = tools[0]
            try:
                await tool.ainvoke({})
            except Exception:
                pass

            # The critical check: can we still do async work?
            await asyncio.sleep(0.1)
            result = await asyncio.get_running_loop().run_in_executor(
                None, lambda: "still alive"
            )
            assert result == "still alive"

        finally:
            await mgr.stop()

    @pytest.mark.asyncio
    async def test_manager_stop_after_server_death(self):
        """MCPManager.stop() should not crash when server is dead."""
        from icarus.mcp.client import MCPManager

        server = ManagedMCPServer()
        server.start()

        mgr = MCPManager({"test": {"url": server.url}})
        tools, failed = await mgr.start()
        assert failed == []

        # Kill server then stop manager
        server.stop()
        # Should not raise
        await mgr.stop()

    @pytest.mark.asyncio
    async def test_abandon_manager_no_crash(self):
        """Dropping MCPManager reference after server death should not crash.

        This reproduces the exact sidecar crash: abandon manager → GC
        collects async generators → anyio cancel scope injects
        CancelledError into current task → process dies.
        """
        from icarus.mcp.client import MCPManager

        server = ManagedMCPServer()
        server.start()

        mgr = MCPManager({"test": {"url": server.url}})
        tools, failed = await mgr.start()
        assert failed == []

        # Kill server
        server.stop()

        # Abandon — drop references without cleanup.
        # With task isolation in _open_session, the cancel scope is
        # on the connection task, not ours.  GC is safe.
        mgr = None
        tools = None

        import gc
        gc.collect()

        # Event loop should still work — no CancelledError
        await asyncio.sleep(0.2)
        result = await asyncio.get_running_loop().run_in_executor(
            None, lambda: "alive"
        )
        assert result == "alive"
