"""
Test integracyjny: uruchamia mcp_test_server.py, aktualizuje jego kod
(dodaje nową metodę), czeka na restart i weryfikuje że nowa metoda działa.
"""
import asyncio
import os
import shutil
import subprocess
import sys
import tempfile
import time

import socket

import httpx
import pytest
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SERVER_TEMPLATE = os.path.join(os.path.dirname(__file__), "mcp_test_server.py")
VENV_PYTHON = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".venv", "bin", "python")


def _free_port() -> int:
    """Zwraca wolny port TCP na localhost."""
    with socket.socket() as s:
        s.bind(("localhost", 0))
        return s.getsockname()[1]


async def _wait_for_server(url: str, timeout: float = 10.0, interval: float = 0.3) -> bool:
    deadline = time.time() + timeout
    async with httpx.AsyncClient() as client:
        while time.time() < deadline:
            try:
                r = await client.post(url, json={}, timeout=1.0)
                return True
            except Exception:
                await asyncio.sleep(interval)
    return False


async def _wait_for_server_down(url: str, timeout: float = 5.0, interval: float = 0.2) -> bool:
    deadline = time.time() + timeout
    async with httpx.AsyncClient() as client:
        while time.time() < deadline:
            try:
                await client.post(url, json={}, timeout=0.5)
                await asyncio.sleep(interval)
            except Exception:
                return True
    return False


async def _list_tool_names(url: str) -> list[str]:
    async with streamablehttp_client(url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.list_tools()
            return [t.name for t in result.tools]


async def _call_tool(url: str, name: str, arguments: dict) -> str:
    async with streamablehttp_client(url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool(name, arguments)
            return result.content[0].text


@pytest.fixture()
def server_file(tmp_path):
    """Kopiuje szablon serwera do katalogu tymczasowego."""
    dest = tmp_path / "mcp_test_server.py"
    shutil.copy(SERVER_TEMPLATE, dest)
    return str(dest)


@pytest.fixture()
def running_server(server_file):
    """Uruchamia serwer na wolnym porcie i zatrzymuje go po teście."""
    port = _free_port()
    url = f"http://localhost:{port}/mcp/"

    env = os.environ.copy()
    env["PYTHONPATH"] = REPO_ROOT
    env["MCP_PORT"] = str(port)

    proc = subprocess.Popen(
        [VENV_PYTHON, server_file],
        env=env,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )

    ready = asyncio.run(_wait_for_server(url, timeout=20.0))
    assert ready, "Serwer nie wystartował w czasie"

    yield proc, server_file, url

    # sprzątanie — zabij wszystkie procesy na porcie (po execv PID może się zmienić)
    os.system(f"fuser -k {port}/tcp 2>/dev/null || true")
    try:
        proc.kill()
    except ProcessLookupError:
        pass  # już martwy (execv zastąpił PID)
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        pass
    # poczekaj aż port zostanie zwolniony
    import time as _time
    deadline = _time.time() + 10.0
    while _time.time() < deadline:
        with socket.socket() as s:
            if s.connect_ex(("localhost", port)) != 0:
                break
        _time.sleep(0.1)


@pytest.mark.asyncio
async def test_server_has_basic_tools(running_server):
    _, _, url = running_server
    tools = await _list_tool_names(url)
    assert "readCode" in tools
    assert "updateCode" in tools
    assert "hello" in tools


@pytest.mark.asyncio
async def test_update_adds_new_method(running_server):
    _, server_file, url = running_server

    # pobierz aktualny kod
    current_code = await _call_tool(url, "readCode", {})
    assert "def hello" in current_code

    # dodaj nową metodę przed blokiem if __name__
    new_method = '''
@mcp.tool()
def brand_new_method() -> str:
    """Nowa metoda dodana przez test"""
    return "brand new!"

'''
    new_code = current_code.replace(
        'if __name__ == "__main__":',
        new_method + 'if __name__ == "__main__":',
    )

    # wyślij nowy kod — serwer zrestartuje się
    result = await _call_tool(url, "updateCode", {"new_code": new_code})
    assert "zaktualizowany" in result

    # poczekaj aż serwer zejdzie i wróci
    down = await _wait_for_server_down(url, timeout=5.0)
    assert down, "Serwer nie zszedł po updateCode"

    up = await _wait_for_server(url, timeout=15.0)
    assert up, "Serwer nie wystartował ponownie po aktualizacji"

    # sprawdź że nowa metoda jest dostępna
    tools = await _list_tool_names(url)
    assert "brand_new_method" in tools


@pytest.mark.asyncio
async def test_new_method_returns_correct_value(running_server):
    _, server_file, url = running_server

    current_code = await _call_tool(url, "readCode", {})

    new_method = '''
@mcp.tool()
def brand_new_method() -> str:
    """Nowa metoda dodana przez test"""
    return "brand new!"

'''
    new_code = current_code.replace(
        'if __name__ == "__main__":',
        new_method + 'if __name__ == "__main__":',
    )

    await _call_tool(url, "updateCode", {"new_code": new_code})

    await _wait_for_server_down(url, timeout=5.0)
    up = await _wait_for_server(url, timeout=15.0)
    assert up

    result = await _call_tool(url, "brand_new_method", {})
    assert result == "brand new!"
