import asyncio
import sys
import textwrap
from unittest.mock import AsyncMock, patch

import pytest

from app.agent.mcp import AgentMcpManager, AgentMcpToolSpec, _StdioMcpSession
from app.agent.tools.catalog import ToolCatalogSnapshot
from app.agent.tools.impl.mcp import (
    McpExternalTool,
    create_external_mcp_tools,
    select_legacy_mcp_tools,
)
from app.schemas.agent import AgentMcpServerConfig


def _write_stdio_mcp_server(tmp_path):
    """写入一个用于测试的最小 stdio MCP 服务。"""
    server_path = tmp_path / "stdio_mcp_server.py"
    server_path.write_text(
        textwrap.dedent(
            """
            import json
            import sys

            TOOLS = [
                {
                    "name": "echo",
                    "description": "Echo input text.",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "text": {"type": "string", "description": "Text to echo"}
                        },
                        "required": ["text"],
                    },
                }
            ]

            for line in sys.stdin:
                request = json.loads(line)
                request_id = request.get("id")
                method = request.get("method")
                if request_id is None:
                    continue
                if method == "initialize":
                    result = {
                        "protocolVersion": request["params"]["protocolVersion"],
                        "capabilities": {"tools": {}},
                        "serverInfo": {"name": "Fake MCP", "version": "1.0.0"},
                    }
                elif method == "tools/list":
                    result = {"tools": TOOLS}
                elif method == "tools/call":
                    args = request.get("params", {}).get("arguments", {})
                    result = {"content": [{"type": "text", "text": args.get("text", "")}]}
                else:
                    result = {}
                print(json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}), flush=True)
            """
        ),
        encoding="utf-8",
    )
    return server_path


@pytest.mark.anyio
async def test_stdio_mcp_server_lists_tools(tmp_path):
    """stdio MCP 服务器应能被初始化并读取工具列表。"""
    server_path = _write_stdio_mcp_server(tmp_path)
    manager = AgentMcpManager()
    server = AgentMcpServerConfig(
        id="fake",
        name="Fake MCP",
        transport="stdio",
        command=sys.executable,
        args=[str(server_path)],
        timeout=5,
    )

    tools = await manager.list_server_tools(server)

    assert len(tools) == 1
    assert tools[0].name == "echo"
    assert tools[0].agent_tool_name == "mcp_fake_mcp_echo"
    assert tools[0].input_schema["properties"]["text"]["type"] == "string"


@pytest.mark.anyio
async def test_stdio_mcp_session_waits_for_stderr_reader_shutdown() -> None:
    """stdio MCP 会话退出时必须等待 stderr reader 真正完成。"""
    session = _StdioMcpSession(
        AgentMcpServerConfig(id="fake", name="Fake MCP", transport="stdio")
    )
    started = asyncio.Event()
    cancelled = asyncio.Event()

    async def reader() -> None:
        """模拟持续读取 stderr 的会话子任务。"""
        started.set()
        try:
            await asyncio.Event().wait()
        except asyncio.CancelledError:
            cancelled.set()
            raise

    session.stderr_task = asyncio.create_task(reader())
    await started.wait()

    await session.__aexit__(None, None, None)

    assert session.stderr_task is None
    assert cancelled.is_set()


@pytest.mark.anyio
async def test_enabled_specs_preserve_cross_server_name_collisions() -> None:
    """跨 MCP 服务器生成相同 Agent 名时必须把全部身份交给目录判断。"""
    manager = AgentMcpManager()
    first = AgentMcpServerConfig(id="one", name="one", transport="stdio", command="one")
    second = AgentMcpServerConfig(id="two", name="two", transport="stdio", command="two")

    def _spec(server):
        return AgentMcpToolSpec(
            server=server,
            name="echo",
            agent_tool_name="shared_echo",
            description="echo",
            input_schema={"type": "object", "properties": {}},
        )

    with patch.object(manager, "get_servers", return_value=[first, second]), patch.object(
        manager,
        "list_server_tools",
        new=AsyncMock(side_effect=lambda server: [_spec(server)]),
    ):
        specs = await manager.list_enabled_tool_specs()

    assert [spec.server.id for spec in specs] == ["one", "two"]


@pytest.mark.anyio
async def test_mcp_catalog_uses_server_id_and_legacy_execution_keeps_first() -> None:
    """目录应区分同显示名服务器，普通执行仍保留首个同名工具。"""
    first_server = AgentMcpServerConfig(
        id="one",
        name="Shared Name",
        transport="stdio",
        command="one",
    )
    second_server = AgentMcpServerConfig(
        id="two",
        name="Shared Name",
        transport="stdio",
        command="two",
    )

    def _spec(server):
        return AgentMcpToolSpec(
            server=server,
            name="echo",
            agent_tool_name="shared_echo",
            description="echo",
            input_schema={"type": "object", "properties": {}},
        )

    tools = await create_external_mcp_tools(
        session_id="session",
        user_id="user",
        specs=[_spec(first_server), _spec(second_server)],
    )
    catalog = ToolCatalogSnapshot.from_tools(
        tools,
        plugin_revision=0,
        factory_revision="factory-v1",
    )

    assert [entry.source for entry in catalog.collisions["shared_echo"]] == [
        "mcp:one",
        "mcp:two",
    ]
    assert len({entry.identity for entry in catalog.collisions["shared_echo"]}) == 2
    assert select_legacy_mcp_tools(tools) == [tools[0]]


@pytest.mark.anyio
async def test_mcp_catalog_distinguishes_normalized_names_within_server() -> None:
    """同一服务内规范化重名的原始工具仍应具有不同绑定身份。"""
    server = AgentMcpServerConfig(
        id="shared",
        name="Shared",
        transport="stdio",
        command="shared",
    )

    def _spec(name: str) -> AgentMcpToolSpec:
        return AgentMcpToolSpec(
            server=server,
            name=name,
            agent_tool_name="mcp_shared_foo_bar",
            description="same description",
            input_schema={"type": "object", "properties": {}},
        )

    tools = await create_external_mcp_tools(
        session_id="session",
        user_id="user",
        specs=[_spec("foo-bar"), _spec("foo_bar")],
    )
    catalog = ToolCatalogSnapshot.from_tools(
        tools,
        plugin_revision=0,
        factory_revision="factory-v1",
    )
    first_only = ToolCatalogSnapshot.from_tools(
        [tools[0]],
        plugin_revision=0,
        factory_revision="factory-v1",
    )
    second_only = ToolCatalogSnapshot.from_tools(
        [tools[1]],
        plugin_revision=0,
        factory_revision="factory-v1",
    )

    collisions = catalog.collisions["mcp_shared_foo_bar"]
    assert len({entry.identity for entry in collisions}) == 2
    assert len({entry.revision.implementation for entry in collisions}) == 2
    assert first_only.signature != second_only.signature


@pytest.mark.anyio
async def test_stdio_mcp_server_calls_tool(tmp_path):
    """stdio MCP 工具应能通过 tools/call 返回内容。"""
    server_path = _write_stdio_mcp_server(tmp_path)
    manager = AgentMcpManager()
    server = AgentMcpServerConfig(
        id="fake",
        name="Fake MCP",
        transport="stdio",
        command=sys.executable,
        args=[str(server_path)],
        timeout=5,
    )

    result = await manager.call_server_tool(server, "echo", {"text": "hello"})

    assert result == {"content": [{"type": "text", "text": "hello"}]}


def test_normalize_server_generates_runtime_defaults():
    """MCP 配置规范化应补齐默认值并清理空字段。"""
    manager = AgentMcpManager()

    server = manager.normalize_server(
        {
            "id": "demo",
            "name": "Demo",
            "transport": "http",
            "url": " https://example.com/mcp ",
            "headers": {" Authorization ": "Bearer token", "": "ignored"},
            "timeout": "bad",
        }
    )

    assert server.id == "demo"
    assert server.url == "https://example.com/mcp"
    assert server.headers == {"Authorization": "Bearer token"}
    assert server.timeout == 30
    assert server.require_admin is True


def test_mcp_external_tool_uses_discovered_schema():
    """外部 MCP 工具应保留发现到的 JSON Schema。"""
    server = AgentMcpServerConfig(id="fake", name="Fake MCP")
    spec = AgentMcpToolSpec(
        server=server,
        name="echo",
        agent_tool_name="mcp_fake_echo",
        description="Echo input text.",
        input_schema={
            "type": "object",
            "properties": {"text": {"type": "string"}},
            "required": ["text"],
        },
    )

    tool = McpExternalTool(spec=spec, session_id="session-1", user_id="10001")

    assert tool.name == "mcp_fake_echo"
    assert tool.args_schema["properties"]["text"]["type"] == "string"
    assert tool.require_admin is True
