"""
Testy notyfikacji MCP tools/list_changed.

Weryfikują że podłączony klient MCP otrzymuje notyfikację tools/list_changed
gdy Orchestrator dodaje lub usuwa backend — bez potrzeby odpytywania serwera.
"""

import asyncio
import threading
import time

import httpx
import pytest
import uvicorn
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.types import ServerNotification, ToolListChangedNotification

from ..connection_pool import ConnectionPool
from ..executor import Executor
from ..gateway import Gateway
from ..orchestrator import Orchestrator
from ..registry import Registry

GATEWAY_HOST = "127.0.0.1"
GATEWAY_PORT = 18129
GATEWAY_URL = f"http://{GATEWAY_HOST}:{GATEWAY_PORT}/mcp/"


@pytest.fixture(scope="module")
def infrastructure(mcp_server_url):
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    gateway = Gateway(registry, executor)
    orchestrator = Orchestrator(registry, pool, executor, gateway=gateway)

    app = gateway.create_app()
    config = uvicorn.Config(app, host=GATEWAY_HOST, port=GATEWAY_PORT, log_level="warning")
    server = uvicorn.Server(config)
    thread = threading.Thread(target=server.run, daemon=True)
    thread.start()

    deadline = time.time() + 5
    while time.time() < deadline:
        try:
            httpx.get(f"http://{GATEWAY_HOST}:{GATEWAY_PORT}/")
            break
        except Exception:
            time.sleep(0.1)

    yield {"orchestrator": orchestrator, "registry": registry, "mcp_server_url": mcp_server_url}

    server.should_exit = True


@pytest.fixture
async def system(infrastructure):
    yield infrastructure
    orch = infrastructure["orchestrator"]
    registry = infrastructure["registry"]
    for backend_id in list(registry._backends.keys()):
        await orch.remove_backend(backend_id)


def make_notification_collector(session: ClientSession) -> asyncio.Event:
    """Podmienia _received_notification na wersję która ustawia Event przy ToolListChanged."""
    received = asyncio.Event()
    original = session._received_notification

    async def patched(notification: ServerNotification) -> None:
        if isinstance(notification.root, ToolListChangedNotification):
            received.set()
        await original(notification)

    session._received_notification = patched
    return received


@pytest.mark.anyio
async def test_client_notified_when_backend_added(system):
    async with streamable_http_client(GATEWAY_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            await session.list_tools()  # rejestruje sesję w Gateway

            notified = make_notification_collector(session)
            await system["orchestrator"].add_backend("test", system["mcp_server_url"])

            await asyncio.wait_for(notified.wait(), timeout=3.0)
            assert notified.is_set()


@pytest.mark.anyio
async def test_client_notified_when_backend_removed(system):
    await system["orchestrator"].add_backend("test", system["mcp_server_url"])

    async with streamable_http_client(GATEWAY_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            await session.list_tools()

            notified = make_notification_collector(session)
            await system["orchestrator"].remove_backend("test")

            await asyncio.wait_for(notified.wait(), timeout=3.0)
            assert notified.is_set()


@pytest.mark.anyio
async def test_after_notification_list_tools_returns_updated_tools(system):
    """Po otrzymaniu notyfikacji klient widzi zaktualizowaną listę narzędzi."""
    async with streamable_http_client(GATEWAY_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            before = await session.list_tools()
            assert before.tools == []

            notified = make_notification_collector(session)
            await system["orchestrator"].add_backend("test", system["mcp_server_url"])
            await asyncio.wait_for(notified.wait(), timeout=3.0)

            after = await session.list_tools()
            names = [t.name for t in after.tools]
            assert "test__echo" in names


@pytest.mark.anyio
async def test_no_notification_without_gateway(system):
    """Orchestrator bez gateway nie wysyła notyfikacji — nie rzuca wyjątku."""
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    orchestrator_no_gw = Orchestrator(registry, pool, executor, gateway=None)

    # nie powinno rzucić
    await orchestrator_no_gw.add_backend("test", system["mcp_server_url"])
    await orchestrator_no_gw.remove_backend("test")


@pytest.mark.anyio
async def test_disconnected_session_does_not_crash_notify(system):
    """Notyfikacja do rozłączonej sesji jest ignorowana, nie crashuje systemu."""
    async with streamable_http_client(GATEWAY_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            await session.list_tools()
    # sesja jest teraz rozłączona (WeakSet może ją jeszcze trzymać chwilę)

    # nie powinno rzucić
    await system["orchestrator"].add_backend("test", system["mcp_server_url"])
