"""Tests for MCPClient."""
import pytest
from unittest.mock import AsyncMock, MagicMock

from yier_agents.src.mcp_client import MCPClient, _make_params_model
from yier_agents.src.tool import Tool, ToolContext


# ── helpers ──────────────────────────────────────────────────────────────────

def make_mcp_tool(name="fetch", description="Fetch a URL", input_schema=None):
    """Return a fake MCP tool object."""
    tool = MagicMock()
    tool.name = name
    tool.description = description
    tool.inputSchema = input_schema or {
        "type": "object",
        "properties": {"url": {"type": "string"}},
        "required": ["url"],
    }
    return tool


def make_text_content(text: str):
    """Return a fake MCP TextContent object."""
    content = MagicMock()
    content.text = text
    return content


def make_client_with_session():
    """Return an MCPClient with a mocked _session (skips network)."""
    client = MCPClient("stdio", command="uvx", args=[])
    client._session = AsyncMock()
    return client


# ── _make_params_model ────────────────────────────────────────────────────────

class TestMakeParamsModel:
    def test_name_is_set(self):
        model = _make_params_model("my_tool", {})
        assert model.__name__ == "my_tool_params"

    def test_schema_is_preserved(self):
        schema = {"type": "object", "properties": {"url": {"type": "string"}}}
        model = _make_params_model("my_tool", schema)
        assert model.model_json_schema() == schema

    def test_accepts_extra_fields(self):
        model = _make_params_model("my_tool", {})
        instance = model.model_validate({"url": "https://example.com", "x": 1})
        assert instance.url == "https://example.com"  # type: ignore[attr-defined]


# ── _convert_tool ─────────────────────────────────────────────────────────────

class TestConvertTool:
    def test_returns_tool_instance(self):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())
        assert isinstance(tool, Tool)

    def test_name_and_description_mapped(self):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool(name="fetch", description="Fetch a URL"))
        assert tool.name == "fetch"
        assert tool.description == "Fetch a URL"

    def test_openai_schema_uses_mcp_input_schema(self):
        client = make_client_with_session()
        schema = {"type": "object", "properties": {"url": {"type": "string"}}}
        tool = client._convert_tool(make_mcp_tool(input_schema=schema))
        assert tool.to_openai_schema()["function"]["parameters"] == schema


# ── execute (via _convert_tool) ───────────────────────────────────────────────

class TestExecute:
    @pytest.fixture
    def ctx(self):
        return ToolContext(session_id="s", message_id="m", call_id="c")

    async def test_execute_calls_call_tool_with_arguments(self, ctx):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())

        result = MagicMock()
        result.isError = False
        result.content = [make_text_content("ok")]
        client._session.call_tool = AsyncMock(return_value=result)

        params = tool.parameters.model_validate({"url": "https://example.com"})
        await tool.execute(params, ctx)

        client._session.call_tool.assert_awaited_once_with(
            "fetch", {"url": "https://example.com"}
        )

    async def test_execute_returns_text_content(self, ctx):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())

        result = MagicMock()
        result.isError = False
        result.content = [make_text_content("Hello")]
        client._session.call_tool = AsyncMock(return_value=result)

        params = tool.parameters.model_validate({"url": "https://example.com"})
        output = await tool.execute(params, ctx)

        assert output.content == "Hello"

    async def test_execute_joins_multiple_text_contents(self, ctx):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())

        result = MagicMock()
        result.isError = False
        result.content = [make_text_content("Part 1"), make_text_content("Part 2")]
        client._session.call_tool = AsyncMock(return_value=result)

        params = tool.parameters.model_validate({"url": "https://example.com"})
        output = await tool.execute(params, ctx)

        assert output.content == "Part 1\nPart 2"

    async def test_execute_ignores_non_text_content(self, ctx):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())

        image = MagicMock(spec=[])          # no .text attribute
        result = MagicMock()
        result.isError = False
        result.content = [image, make_text_content("text only")]
        client._session.call_tool = AsyncMock(return_value=result)

        params = tool.parameters.model_validate({"url": "https://example.com"})
        output = await tool.execute(params, ctx)

        assert output.content == "text only"

    async def test_execute_raises_on_mcp_error(self, ctx):
        client = make_client_with_session()
        tool = client._convert_tool(make_mcp_tool())

        result = MagicMock()
        result.isError = True
        result.content = [make_text_content("Something went wrong")]
        client._session.call_tool = AsyncMock(return_value=result)

        params = tool.parameters.model_validate({"url": "https://example.com"})

        with pytest.raises(RuntimeError, match="Something went wrong"):
            await tool.execute(params, ctx)


# ── get_tools ─────────────────────────────────────────────────────────────────

class TestGetTools:
    async def test_returns_tool_list(self):
        client = make_client_with_session()

        list_result = MagicMock()
        list_result.tools = [
            make_mcp_tool(name="fetch", description="Fetch"),
            make_mcp_tool(name="search", description="Search"),
        ]
        client._session.list_tools = AsyncMock(return_value=list_result)

        tools = await client.get_tools()

        assert len(tools) == 2
        assert tools[0].name == "fetch"
        assert tools[1].name == "search"
        client._session.list_tools.assert_awaited_once()

    async def test_empty_server_returns_empty_list(self):
        client = make_client_with_session()

        list_result = MagicMock()
        list_result.tools = []
        client._session.list_tools = AsyncMock(return_value=list_result)

        tools = await client.get_tools()
        assert tools == []
