"""U16 — MCPClient langchain-mcp-adapters 路径单元测试。

覆盖场景：
1. URL scheme 检测（http / sse / stdio）
2. list_tools via langchain（三种传输）
3. call_tool via langchain（正常 / 未知工具）
4. 旧 Transport 路径兼容（DeprecationWarning）
5. MCPTool.execute via 新客户端（JSON / 纯文本 / 无 content）
6. langchain 未安装时 ImportError
7. transport.py 模块级 DeprecationWarning
8. timeout 参数透传
"""

from __future__ import annotations

import importlib
import sys
import warnings
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agentkit.mcp.client import MCPClient, MCPTool
from agentkit.mcp.transport import HTTPTransport


# ── 辅助工具 ──────────────────────────────────────────────────


def _make_mock_langchain_tool(
    name: str = "my_tool",
    description: str = "Test tool",
    result: Any = None,
) -> MagicMock:
    """构造一个假的 LangChain Tool 对象。"""
    tool = MagicMock()
    tool.name = name
    tool.description = description
    tool.args_schema = MagicMock()
    tool.args_schema.model_json_schema.return_value = {
        "type": "object",
        "properties": {},
    }
    tool.ainvoke = AsyncMock(return_value=result if result is not None else {"result": "ok"})
    return tool


@pytest.fixture
def mock_langchain_client() -> Any:
    """Patch MultiServerMCPClient，返回带 get_tools 的 mock 实例。

    Yields:
        (mock_class, mock_instance) 二元组
    """
    with patch("agentkit.mcp.client._import_langchain_client") as mock_import:
        mock_cls = MagicMock(name="MultiServerMCPClient")
        mock_instance = MagicMock(name="mcp_client_instance")
        mock_instance.get_tools = AsyncMock(return_value=[])
        mock_cls.return_value = mock_instance
        mock_import.return_value = mock_cls
        yield mock_cls, mock_instance


# ── URL scheme 检测测试 ───────────────────────────────────────


class TestURLSchemeDetection:
    """MCPClient 根据 URL scheme 自动检测传输类型。"""

    def test_http_url_uses_streamable_http(self):
        """http:// URL 应使用 streamable_http 传输。"""
        client = MCPClient("http://localhost:8080/mcp")
        config = client._langchain_config
        assert config is not None
        assert config["transport"] == "streamable_http"
        assert config["url"] == "http://localhost:8080/mcp"

    def test_https_url_uses_streamable_http(self):
        """https:// URL 应使用 streamable_http 传输。"""
        client = MCPClient("https://remote.example.com/mcp")
        config = client._langchain_config
        assert config["transport"] == "streamable_http"
        assert config["url"] == "https://remote.example.com/mcp"

    def test_sse_url_uses_sse_transport(self):
        """sse:// URL 应使用 sse 传输，并转为 http://。"""
        client = MCPClient("sse://localhost:8080/sse")
        config = client._langchain_config
        assert config["transport"] == "sse"
        assert config["url"] == "http://localhost:8080/sse"

    def test_stdio_url_parses_command_and_args(self):
        """stdio:// URL 应解析为 command + args。"""
        client = MCPClient("stdio://python -m my_mcp_server")
        config = client._langchain_config
        assert config["transport"] == "stdio"
        assert config["command"] == "python"
        assert config["args"] == ["-m", "my_mcp_server"]

    def test_stdio_url_no_args(self):
        """stdio:// 只有 command 无参数。"""
        client = MCPClient("stdio://./server")
        config = client._langchain_config
        assert config["command"] == "./server"
        assert config["args"] == []

    def test_unsupported_scheme_raises(self):
        """不支持的 scheme 应抛出 ValueError。"""
        with pytest.raises(ValueError, match="不支持的 URL scheme"):
            MCPClient("ftp://example.com/server")

    def test_empty_url_raises(self):
        """空 URL 应抛出 ValueError。"""
        with pytest.raises(ValueError, match="server_url 不能为空"):
            MCPClient("")


# ── list_tools via langchain 测试 ─────────────────────────────


class TestListToolsViaLangchain:
    """list_tools 通过 langchain-mcp-adapters 获取工具列表。"""

    async def test_list_tools_http(self, mock_langchain_client):
        """HTTP 传输 — list_tools 返回工具字典列表。"""
        mock_cls, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(
            return_value=[
                _make_mock_langchain_tool("search", "Search tool"),
                _make_mock_langchain_tool("calc", "Calculator"),
            ]
        )

        client = MCPClient("http://localhost:8080/mcp")
        tools = await client.list_tools()

        assert len(tools) == 2
        assert tools[0]["name"] == "search"
        assert tools[0]["description"] == "Search tool"
        assert "inputSchema" in tools[0]
        assert tools[1]["name"] == "calc"

    async def test_list_tools_stdio(self, mock_langchain_client):
        """Stdio 传输 — list_tools 返回工具字典列表。"""
        mock_cls, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(
            return_value=[_make_mock_langchain_tool("read_file", "Read file")]
        )

        client = MCPClient("stdio://python -m fs_server")
        tools = await client.list_tools()

        assert len(tools) == 1
        assert tools[0]["name"] == "read_file"

    async def test_list_tools_sse(self, mock_langchain_client):
        """SSE 传输 — list_tools 返回工具字典列表。"""
        mock_cls, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(
            return_value=[_make_mock_langchain_tool("query", "Query data")]
        )

        client = MCPClient("sse://localhost:8080/sse")
        tools = await client.list_tools()

        assert len(tools) == 1
        assert tools[0]["name"] == "query"

    async def test_list_tools_caches_result(self, mock_langchain_client):
        """list_tools 结果应缓存到 _tools_cache。"""
        _, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(return_value=[_make_mock_langchain_tool("tool1")])

        client = MCPClient("http://localhost:8080/mcp")
        tools = await client.list_tools()

        assert client._tools_cache == tools
        assert client._tools_cache[0]["name"] == "tool1"

    async def test_list_tools_empty(self, mock_langchain_client):
        """空工具列表应返回 []。"""
        _, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(return_value=[])

        client = MCPClient("http://localhost:8080/mcp")
        tools = await client.list_tools()
        assert tools == []


# ── call_tool via langchain 测试 ──────────────────────────────


class TestCallToolViaLangchain:
    """call_tool 通过 langchain-mcp-adapters 调用工具。"""

    async def test_call_tool_returns_mcp_format(self, mock_langchain_client):
        """call_tool 应返回 MCP 响应格式 {"content": [{"type":"text","text":...}]}。"""
        _, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(
            return_value=[_make_mock_langchain_tool("my_tool", result={"result": "hello"})]
        )

        client = MCPClient("http://localhost:8080/mcp")
        result = await client.call_tool("my_tool", {"input": "x"})

        assert "content" in result
        assert result["content"][0]["type"] == "text"
        assert '"result": "hello"' in result["content"][0]["text"]

    async def test_call_tool_invokes_ainvoke(self, mock_langchain_client):
        """call_tool 应调用 LangChain Tool 的 ainvoke。"""
        _, mock_instance = mock_langchain_client
        mock_tool = _make_mock_langchain_tool("my_tool", result={"ok": True})
        mock_instance.get_tools = AsyncMock(return_value=[mock_tool])

        client = MCPClient("http://localhost:8080/mcp")
        await client.call_tool("my_tool", {"input": "x"})

        mock_tool.ainvoke.assert_called_once_with({"input": "x"})

    async def test_call_tool_unknown_raises(self, mock_langchain_client):
        """调用不存在的工具应抛出 KeyError。"""
        _, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(return_value=[])

        client = MCPClient("http://localhost:8080/mcp")
        with pytest.raises(KeyError, match="nonexistent"):
            await client.call_tool("nonexistent", {})


# ── 旧 Transport 路径兼容测试 ─────────────────────────────────


class TestLegacyTransportPath:
    """旧 Transport 注入路径应保持向后兼容（发出 DeprecationWarning）。"""

    def test_from_transport_emits_deprecation_warning(self):
        """from_transport 应发出 DeprecationWarning。"""
        transport = HTTPTransport(endpoint="http://localhost:8080/mcp")
        with pytest.warns(DeprecationWarning, match="Transport"):
            client = MCPClient.from_transport(transport)
        assert client._transport is transport
        assert client._langchain_config is None

    def test_init_with_transport_emits_deprecation_warning(self):
        """__init__ 传入 transport 应发出 DeprecationWarning。"""
        transport = HTTPTransport(endpoint="http://localhost:8080/mcp")
        with pytest.warns(DeprecationWarning):
            client = MCPClient(server_url="http://localhost:8080", transport=transport)
        assert client._transport is transport

    async def test_legacy_list_tools_works(self):
        """旧 Transport 路径的 list_tools 仍应正常工作。"""
        transport = HTTPTransport(endpoint="http://localhost:8080")
        transport._client = MagicMock()
        transport._client.is_closed = False
        transport.send_request = AsyncMock(
            return_value={"tools": [{"name": "legacy_tool", "description": "Legacy"}]}
        )

        with pytest.warns(DeprecationWarning):
            client = MCPClient.from_transport(transport)

        tools = await client.list_tools()
        assert len(tools) == 1
        assert tools[0]["name"] == "legacy_tool"
        transport.send_request.assert_called_once_with("tools/list")


# ── MCPTool.execute 测试 ──────────────────────────────────────


class TestMCPToolExecute:
    """MCPTool.execute 通过新客户端调用工具并解析响应。"""

    async def test_execute_parses_json_text(self, mock_langchain_client):
        """execute 应解析 content[0].text 中的 JSON。"""
        _, mock_instance = mock_langchain_client
        mock_instance.get_tools = AsyncMock(
            return_value=[_make_mock_langchain_tool("my_tool", result={"answer": 42})]
        )

        client = MCPClient("http://localhost:8080/mcp")
        tool = client.as_tool("my_tool", "desc")
        result = await tool.execute(input="x")

        assert result == {"answer": 42}

    async def test_execute_non_json_text(self):
        """content[0].text 为纯文本时返回 {"result": text}。"""
        client = MCPClient("http://localhost:8080/mcp")
        client.call_tool = AsyncMock(  # type: ignore[method-assign]
            return_value={"content": [{"type": "text", "text": "plain text"}]}
        )
        tool = client.as_tool("echo", "desc")

        result = await tool.execute(msg="hello")
        assert result == {"result": "plain text"}

    async def test_execute_no_content_field(self):
        """无 content 字段时返回原始 dict。"""
        client = MCPClient("http://localhost:8080/mcp")
        client.call_tool = AsyncMock(  # type: ignore[method-assign]
            return_value={"unexpected": "shape"}
        )
        tool = client.as_tool("status", "desc")

        result = await tool.execute()
        assert result == {"unexpected": "shape"}

    async def test_as_tool_returns_mcp_tool(self):
        """as_tool 应返回 MCPTool 实例。"""
        client = MCPClient("http://localhost:8080/mcp")
        tool = client.as_tool("search", "Search the web")

        assert isinstance(tool, MCPTool)
        assert tool.name == "search"
        assert tool.description == "Search the web"
        assert tool._client is client
        assert "mcp" in tool.tags


# ── langchain 未安装测试 ─────────────────────────────────────


class TestLangchainNotInstalled:
    """langchain-mcp-adapters 未安装时的错误处理。"""

    async def test_import_error_when_langchain_missing(self):
        """langchain 未安装时 list_tools 应抛出 ImportError。"""
        client = MCPClient("http://localhost:8080/mcp")

        with patch(
            "agentkit.mcp.client._import_langchain_client",
            side_effect=ImportError("langchain-mcp-adapters 未安装"),
        ):
            with pytest.raises(ImportError, match="langchain-mcp-adapters"):
                await client.list_tools()


# ── transport.py 模块级 DeprecationWarning 测试 ──────────────


class TestTransportDeprecation:
    """transport.py 导入时应发出 DeprecationWarning。"""

    def test_transport_module_emits_deprecation_warning(self):
        """导入 agentkit.mcp.transport 应触发 DeprecationWarning。"""
        # 先从 sys.modules 移除，强制重新导入
        mods_to_remove = [k for k in sys.modules if k.startswith("agentkit.mcp.transport")]
        for mod in mods_to_remove:
            del sys.modules[mod]

        with warnings.catch_warnings(record=True) as warning_list:
            warnings.simplefilter("always")
            importlib.import_module("agentkit.mcp.transport")

        deprecation_warnings = [
            w for w in warning_list if issubclass(w.category, DeprecationWarning)
        ]
        assert len(deprecation_warnings) >= 1
        assert "transport" in str(deprecation_warnings[0].message)


# ── timeout 参数透传测试 ─────────────────────────────────────


class TestTimeoutPropagation:
    """timeout 参数应透传到 langchain 连接配置。"""

    def test_timeout_passed_to_http_config(self):
        """HTTP 传输应将 timeout 写入配置。"""
        client = MCPClient("http://localhost:8080/mcp", timeout=60)
        assert client._langchain_config["timeout"] == 60

    def test_timeout_passed_to_sse_config(self):
        """SSE 传输应将 timeout 写入配置。"""
        client = MCPClient("sse://localhost:8080/sse", timeout=45)
        assert client._langchain_config["timeout"] == 45

    def test_default_timeout_is_30(self):
        """默认 timeout 为 30。"""
        client = MCPClient("http://localhost:8080/mcp")
        assert client._timeout == 30
        assert client._langchain_config["timeout"] == 30

    def test_stdio_config_has_no_timeout(self):
        """stdio 传输配置不含 timeout（StdioConnection 无此字段）。"""
        client = MCPClient("stdio://python -m server", timeout=60)
        assert "timeout" not in client._langchain_config
