"""Testy integracyjne Orchestrator — wymagają działającego serwera MCP."""

import asyncio
from unittest.mock import AsyncMock, MagicMock

import pytest

from ..connection_pool import ConnectionPool
from ..executor import Executor
from ..orchestrator import RECONNECT_INTERVAL, Orchestrator
from ..registry import Registry


@pytest.fixture
def registry():
    return Registry()


@pytest.fixture
def pool():
    return ConnectionPool()


@pytest.fixture
def executor(pool):
    return Executor(pool)


@pytest.fixture
def orchestrator(registry, pool, executor):
    return Orchestrator(registry, pool, executor)


@pytest.mark.anyio
async def test_add_backend_registers_tools(orchestrator, registry, pool, mcp_server_url):
    await orchestrator.add_backend("test", mcp_server_url)

    tools = registry.list_tools()
    names = [t.public_name for t in tools]
    assert "test__echo" in names
    assert "test__add" in names
    assert "test__fail" in names
    await pool.close_all()


@pytest.mark.anyio
async def test_add_backend_stores_backend(orchestrator, registry, pool, mcp_server_url):
    await orchestrator.add_backend("test", mcp_server_url)

    backend = registry.get_backend("test")
    assert backend.id == "test"
    assert backend.url == mcp_server_url
    await pool.close_all()


@pytest.mark.anyio
async def test_add_backend_tool_resolves_correctly(orchestrator, registry, pool, mcp_server_url):
    await orchestrator.add_backend("test", mcp_server_url)

    mapping = registry.resolve("test__echo")
    assert mapping.original_name == "echo"
    assert mapping.backend.id == "test"
    assert mapping.definition.input_schema != {}
    await pool.close_all()


@pytest.mark.anyio
async def test_remove_backend_clears_registry_and_pool(orchestrator, registry, pool, mcp_server_url):
    await orchestrator.add_backend("test", mcp_server_url)
    await orchestrator.remove_backend("test")

    assert registry.list_tools() == []
    assert "test" not in pool._workers


@pytest.mark.anyio
async def test_refresh_backend_updates_tools(orchestrator, registry, pool, mcp_server_url):
    await orchestrator.add_backend("test", mcp_server_url)
    await orchestrator.refresh_backend("test")

    tools = registry.list_tools()
    assert len(tools) > 0
    await pool.close_all()


@pytest.mark.anyio
async def test_two_backends_dont_interfere(orchestrator, registry, pool, mcp_server_url, mcp_server_b_url):
    await orchestrator.add_backend("a", mcp_server_url)
    await orchestrator.add_backend("b", mcp_server_b_url)
    await orchestrator.remove_backend("a")

    tools = registry.list_tools()
    assert all(t.backend.id == "b" for t in tools)
    await pool.close_all()


@pytest.mark.anyio
async def test_handle_disconnect_removes_tools_and_notifies(mcp_server_url):
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    gateway = MagicMock()
    gateway.notify_tool_list_changed = AsyncMock()
    gateway.notify_resource_list_changed = AsyncMock()
    gateway.notify_resource_updated = AsyncMock()
    orchestrator = Orchestrator(registry, pool, executor, gateway=gateway)

    await orchestrator.add_backend("test", mcp_server_url)
    assert len(registry.list_tools()) > 0

    # symuluj rozłączenie
    await orchestrator._handle_disconnect("test")

    assert registry.list_tools() == []
    assert registry.has_backend("test")  # backend nadal zarejestrowany
    gateway.notify_tool_list_changed.assert_called()

    # posprzątaj reconnect task
    task = orchestrator._reconnect_tasks.get("test")
    if task:
        task.cancel()
    await pool.close_all()


@pytest.mark.anyio
async def test_handle_disconnect_ignored_for_unknown_backend():
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    orchestrator = Orchestrator(registry, pool, executor)

    # nie powinno rzucić dla nieznanego backendu
    await orchestrator._handle_disconnect("nieznany")


@pytest.mark.anyio
async def test_remove_backend_cancels_reconnect_task(mcp_server_url):
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    orchestrator = Orchestrator(registry, pool, executor)

    await orchestrator.add_backend("test", mcp_server_url)
    await orchestrator._handle_disconnect("test")

    assert "test" in orchestrator._reconnect_tasks

    await orchestrator.remove_backend("test")

    assert "test" not in orchestrator._reconnect_tasks


@pytest.mark.anyio
async def test_add_backend_unreachable_at_startup_stays_in_registry(orchestrator, registry, pool):
    """Regresja: gdy backend nie odpowiada przy bootcie, zostaje zapisany w registry.

    Wcześniej `add_backend` rejestrował backend DOPIERO PO `list_tools`. Jeśli
    serwer nie żył przy starcie, exception leciał, registry zostawało puste,
    a `save_config` z innego on_change zapisywał plik bez tego backendu —
    przy kolejnym bootcie gateway już go nie znał. Teraz backend jest faktem
    z konfiguracji (registry first), a unreachable serwer wpada w reconnect_loop.
    """
    import mcp_gateway.orchestrator as orch_module
    original_interval = orch_module.RECONNECT_INTERVAL
    orch_module.RECONNECT_INTERVAL = 60.0  # długi — nie chcemy żeby reconnect zadziałał w teście

    try:
        # URL na pewno nieosiągalny — port 1 nie ma nikogo nasłuchującego.
        await orchestrator.add_backend("dead", "http://127.0.0.1:1/mcp")

        # Backend MUSI być w registry, mimo że list_tools rzucił.
        assert registry.has_backend("dead")
        backend = registry.get_backend("dead")
        assert backend.url == "http://127.0.0.1:1/mcp"
        assert backend.enabled is True
        # Tools się nie zarejestrowały (nie udało się ich pobrać).
        assert [t for t in registry.list_tools() if t.backend.id == "dead"] == []
        # Reconnect loop wystartował.
        assert "dead" in orchestrator._reconnect_tasks

        # Cleanup: anuluj reconnect żeby test się nie ciągnął.
        task = orchestrator._reconnect_tasks.pop("dead")
        task.cancel()
        try:
            await task
        except (asyncio.CancelledError, Exception):
            pass

    finally:
        orch_module.RECONNECT_INTERVAL = original_interval
        await pool.close_all()


@pytest.mark.anyio
async def test_reconnect_loop_restores_tools_when_backend_returns(mcp_server_url):
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    gateway = MagicMock()
    gateway.notify_tool_list_changed = AsyncMock()
    gateway.notify_resource_list_changed = AsyncMock()
    gateway.notify_resource_updated = AsyncMock()

    # skrócony interwał reconnectu żeby test był szybki
    import mcp_gateway.orchestrator as orch_module
    original_interval = orch_module.RECONNECT_INTERVAL
    orch_module.RECONNECT_INTERVAL = 0.1

    try:
        orchestrator = Orchestrator(registry, pool, executor, gateway=gateway)
        await orchestrator.add_backend("test", mcp_server_url)

        # symuluj rozłączenie
        await orchestrator._handle_disconnect("test")
        assert registry.list_tools() == []

        # poczekaj na reconnect (serwer testowy nadal działa)
        deadline = asyncio.get_event_loop().time() + 5.0
        while asyncio.get_event_loop().time() < deadline:
            if "test" not in orchestrator._reconnect_tasks:
                break
            await asyncio.sleep(0.05)

        assert len(registry.list_tools()) > 0
        assert "test" not in orchestrator._reconnect_tasks  # task się skończył

    finally:
        orch_module.RECONNECT_INTERVAL = original_interval
        await pool.close_all()
