"""
Testy integracyjne — wiele backendów MCP.

Weryfikują że Gateway i RestApi poprawnie mergują narzędzia z wielu backendów,
każde narzędzie jest dostępne pod prefiksowaną nazwą {backend_id}__{tool_name},
co eliminuje kolizje nazw między backendami.

Backend A (test_server.py):   echo, add, fail
Backend B (test_server_b.py): multiply, upper, echo  ← celowa kolizja nazwy echo

Po rejestracji widoczne jako:
  a__echo, a__add, a__fail, b__multiply, b__upper, b__echo
"""

import threading
import time

import httpx
import pytest
import uvicorn
from httpx import ASGITransport, AsyncClient
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

from ..connection_pool import ConnectionPool
from ..executor import Executor
from ..gateway import Gateway
from ..orchestrator import Orchestrator
from ..registry import Registry
from ..rest_api import RestApi
from .test_server_b import create_app as create_app_b

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

BACKEND_B_HOST = "127.0.0.1"
BACKEND_B_PORT = 18127
BACKEND_B_URL = f"http://{BACKEND_B_HOST}:{BACKEND_B_PORT}/mcp/"


@pytest.fixture(scope="module")
def backend_b_url():
    """Uruchamia drugi testowy serwer MCP (server_b) raz na moduł."""
    config = uvicorn.Config(create_app_b(), host=BACKEND_B_HOST, port=BACKEND_B_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://{BACKEND_B_HOST}:{BACKEND_B_PORT}/")
            break
        except Exception:
            time.sleep(0.1)

    yield BACKEND_B_URL

    server.should_exit = True


@pytest.fixture(scope="module")
def infrastructure(mcp_server_url, backend_b_url):
    """Pełny system ze wspólnym Registry i Gateway jako prawdziwym serwerem."""
    registry = Registry()
    pool = ConnectionPool()
    executor = Executor(pool)
    orchestrator = Orchestrator(registry, pool, executor)
    gateway = Gateway(registry, executor)
    rest_api = RestApi(registry, executor)

    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,
        "rest_app": rest_api.create_app(),
        "gateway_url": GATEWAY_URL,
        "url_a": mcp_server_url,
        "url_b": backend_b_url,
    }

    server.should_exit = True


@pytest.fixture
async def system(infrastructure):
    """Czyści wszystkie backendy po każdym teście."""
    yield infrastructure
    orch = infrastructure["orchestrator"]
    registry = infrastructure["registry"]
    for backend_id in list(registry._backends.keys()):
        await orch.remove_backend(backend_id)


# --- Podstawowe: merge narzędzi ---

@pytest.mark.anyio
async def test_tools_from_both_backends_visible_in_rest_api(system):
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        response = await client.get("/tools")

    names = {t["name"] for t in response.json()}
    assert {"a__echo", "a__add", "a__fail", "b__multiply", "b__upper", "b__echo"} <= names


@pytest.mark.anyio
async def test_tools_from_both_backends_visible_in_gateway(system):
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])

    async with streamable_http_client(system["gateway_url"]) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.list_tools()

    names = {t.name for t in result.tools}
    assert {"a__echo", "a__add", "a__fail", "b__multiply", "b__upper", "b__echo"} <= names


@pytest.mark.anyio
async def test_call_tool_from_each_backend(system):
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        r_a = await client.post("/tools/a__echo", json={"message": "hello"})
        r_b = await client.post("/tools/b__multiply", json={"a": 3, "b": 7})

    assert r_a.json()["content"][0]["text"] == "hello"
    assert r_b.json()["content"][0]["text"] == "21"


# --- Zaawansowane ---

@pytest.mark.anyio
async def test_same_tool_name_no_collision_thanks_to_prefix(system):
    """Oba backendy mają narzędzie 'echo' — prefiks sprawia że są niezależne."""
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        r_a = await client.post("/tools/a__echo", json={"message": "test"})
        r_b = await client.post("/tools/b__echo", json={"message": "test"})

    # server_a zwraca sam tekst, server_b zwraca "server_b: {tekst}"
    assert r_a.json()["content"][0]["text"] == "test"
    assert r_b.json()["content"][0]["text"] == "server_b: test"


@pytest.mark.anyio
async def test_remove_one_backend_leaves_other_intact(system):
    """Usunięcie backendu A nie wpływa na dostępność narzędzi backendu B."""
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])
    await system["orchestrator"].remove_backend("a")

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        tools_response = await client.get("/tools")
        call_b = await client.post("/tools/b__multiply", json={"a": 4, "b": 5})
        call_a = await client.post("/tools/a__echo", json={"message": "gone"})

    names = {t["name"] for t in tools_response.json()}
    assert not any(n.startswith("a__") for n in names)
    assert "b__multiply" in names
    assert call_b.json()["content"][0]["text"] == "20"
    assert call_a.status_code == 404


@pytest.mark.anyio
async def test_refresh_one_backend_leaves_other_intact(system):
    """Refresh backendu A nie usuwa narzędzi backendu B."""
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])
    await system["orchestrator"].refresh_backend("a")

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        tools_response = await client.get("/tools")
        call_b = await client.post("/tools/b__upper", json={"text": "hello"})

    names = {t["name"] for t in tools_response.json()}
    assert "a__echo" in names
    assert "b__upper" in names
    assert call_b.json()["content"][0]["text"] == "HELLO"


@pytest.mark.anyio
async def test_error_in_one_backend_does_not_affect_other(system):
    """Błąd w narzędziu backendu A nie wpływa na narzędzia backendu B."""
    await system["orchestrator"].add_backend("a", system["url_a"])
    await system["orchestrator"].add_backend("b", system["url_b"])

    async with AsyncClient(transport=ASGITransport(app=system["rest_app"]), base_url="http://test") as client:
        fail_result = await client.post("/tools/a__fail", json={})
        ok_result = await client.post("/tools/b__upper", json={"text": "still works"})

    assert fail_result.json()["isError"] is True
    assert ok_result.json()["isError"] is False
    assert ok_result.json()["content"][0]["text"] == "STILL WORKS"
