from types import SimpleNamespace
from unittest.mock import patch

import pytest

from kotaemon.agents.tools.mcp import (
    MCPTool,
    _json_schema_type_to_python,
    _make_tool,
    build_args_model,
    create_tools_from_config,
    format_tool_list,
    parse_mcp_config,
)

# ---------------------------------------------------------------------------
# _json_schema_type_to_python
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("json_type", "expected"),
    [
        ("string", str),
        ("integer", int),
        ("number", float),
        ("boolean", bool),
        ("object", dict),
        ("array", list),
        ("unknown_type", str),  # fallback
    ],
)
def test_json_schema_type_to_python(json_type: str, expected: type) -> None:
    """Maps JSON Schema types to Python types with fallback to str."""
    assert _json_schema_type_to_python(json_type) is expected


# ---------------------------------------------------------------------------
# build_args_model
# ---------------------------------------------------------------------------


def test_build_args_model_fields_and_name() -> None:
    """Required + optional fields and the generated model name."""
    schema = {
        "properties": {
            "url": {"type": "string", "description": "The URL to fetch"},
            "timeout": {"type": "integer", "description": "Timeout in seconds"},
        },
        "required": ["url"],
    }
    model = build_args_model("fetch", schema)
    assert model.__name__ == "MCPArgs_fetch"
    assert model.model_fields["url"].is_required()
    assert not model.model_fields["timeout"].is_required()


def test_build_args_model_optional_field_preserves_default() -> None:
    """Optional field with default preserves that default."""
    schema = {
        "properties": {
            "limit": {
                "type": "integer",
                "description": "Max results",
                "default": 10,
            },
        },
        "required": [],
    }
    assert build_args_model("search", schema).model_fields["limit"].default == 10


def test_build_args_model_empty_schema_produces_no_fields() -> None:
    """Empty schema produces model with no fields."""
    assert len(build_args_model("empty", {}).model_fields) == 0


# ---------------------------------------------------------------------------
# parse_mcp_config
# ---------------------------------------------------------------------------


def test_parse_mcp_config_full_stdio() -> None:
    """Full stdio config is parsed correctly."""
    config = {
        "transport": "stdio",
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "env": {"KEY": "value"},
    }
    parsed = parse_mcp_config(config)
    assert parsed == {
        "transport": "stdio",
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "env": {"KEY": "value"},
    }


def test_parse_mcp_config_defaults_for_empty() -> None:
    """Empty config uses defaults."""
    parsed = parse_mcp_config({})
    assert parsed["transport"] == "stdio"
    assert parsed["command"] == ""
    assert parsed["args"] == []
    assert parsed["env"] == {}


def test_parse_mcp_config_auto_split_multi_word_command() -> None:
    """stdio with no explicit args: space-delimited command is split."""
    parsed = parse_mcp_config({"command": "npx -y mcp-remote https://example.com/sse"})
    assert parsed["command"] == "npx"
    assert parsed["args"] == ["-y", "mcp-remote", "https://example.com/sse"]


def test_parse_mcp_config_no_split_when_args_provided() -> None:
    """Explicit args suppress the auto-split."""
    parsed = parse_mcp_config(
        {
            "command": "npx -y mcp-remote https://example.com/sse",
            "args": ["--flag"],
        }
    )
    assert parsed["command"] == "npx -y mcp-remote https://example.com/sse"
    assert parsed["args"] == ["--flag"]


def test_parse_mcp_config_sse_uses_url_as_command() -> None:
    """For SSE, the url field becomes the effective command."""
    parsed = parse_mcp_config(
        {
            "transport": "sse",
            "url": "http://localhost:8080/sse",
            "command": "ignored",
        }
    )
    assert parsed["transport"] == "sse"
    assert parsed["command"] == "http://localhost:8080/sse"


# ---------------------------------------------------------------------------
# _make_tool
# ---------------------------------------------------------------------------


def test_make_tool_creates_mcp_tool_with_schema() -> None:
    """_make_tool creates MCPTool with correct attributes."""
    parsed = {
        "transport": "stdio",
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "env": {},
    }
    tool_info = SimpleNamespace(
        name="fetch",
        description="Fetch a URL",
        inputSchema={
            "properties": {"url": {"type": "string", "description": "URL to fetch"}},
            "required": ["url"],
        },
    )
    tool = _make_tool(parsed, tool_info)

    assert isinstance(tool, MCPTool)
    assert tool.name == "fetch"
    assert tool.description == "Fetch a URL"
    assert tool.server_transport == "stdio"
    assert tool.server_command == "uvx"
    assert tool.server_args == ["mcp-server-fetch"]


def test_make_tool_missing_schema_and_description_uses_defaults() -> None:
    """No inputSchema -> args_schema is None; None description -> auto-generated."""
    parsed = {"transport": "stdio", "command": "uvx", "args": [], "env": {}}
    tool_info = SimpleNamespace(name="ping", description=None)
    tool = _make_tool(parsed, tool_info)
    assert tool.description == "MCP tool: ping"
    assert tool.args_schema is None


# ---------------------------------------------------------------------------
# format_tool_list
# ---------------------------------------------------------------------------


def test_format_tool_list_all_enabled_by_default() -> None:
    """All tools enabled when no filter specified."""
    tool_infos = [
        {"name": "fetch", "description": "Fetch a URL"},
        {"name": "search", "description": "Search the web"},
    ]
    result = format_tool_list(tool_infos)
    assert "2" in result
    assert "fetch" in result and "search" in result
    assert "All tools enabled" in result


def test_format_tool_list_partial_filter() -> None:
    """Partial filter shows counts and icons."""
    tool_infos = [
        {"name": "fetch", "description": "Fetch a URL"},
        {"name": "search", "description": "Search the web"},
    ]
    result = format_tool_list(tool_infos, enabled_tools=["fetch"])
    assert "1/2 tool(s) enabled" in result
    assert "✅" in result  # fetch enabled
    assert "⬜" in result  # search disabled


def test_format_tool_list_long_description_truncated() -> None:
    """Long descriptions are truncated."""
    result = format_tool_list([{"name": "tool", "description": "A" * 200}])
    assert "A" * 121 not in result


def test_format_tool_list_none_description_shows_placeholder() -> None:
    """None description shows placeholder."""
    result = format_tool_list([{"name": "tool", "description": None}])
    assert "No description" in result


# ---------------------------------------------------------------------------
# create_tools_from_config (mocked MCP server connection)
# ---------------------------------------------------------------------------


def _make_mock_tools() -> list[MCPTool]:
    """Create mock MCPTool list for testing."""
    return [
        MCPTool(
            name="fetch",
            description="Fetch",
            server_transport="stdio",
            server_command="uvx",
            mcp_tool_name="fetch",
        ),
        MCPTool(
            name="search",
            description="Search",
            server_transport="stdio",
            server_command="uvx",
            mcp_tool_name="search",
        ),
    ]


@patch("kotaemon.agents.tools.mcp._run_async")
def test_create_tools_from_config_no_filter_returns_all(mock_run_async) -> None:
    """No filter returns all tools."""
    mock_run_async.return_value = _make_mock_tools()
    tools = create_tools_from_config({"command": "uvx"})
    assert len(tools) == 2


@patch("kotaemon.agents.tools.mcp._run_async")
def test_create_tools_from_config_enabled_filter(mock_run_async) -> None:
    """Non-empty filter returns only nominated tools; empty list returns all."""
    mock_run_async.return_value = _make_mock_tools()
    filtered = create_tools_from_config({"command": "uvx"}, enabled_tools=["fetch"])
    assert len(filtered) == 1
    assert filtered[0].mcp_tool_name == "fetch"

    mock_run_async.return_value = _make_mock_tools()
    all_tools = create_tools_from_config({"command": "uvx"}, enabled_tools=[])
    assert len(all_tools) == 2


# ---------------------------------------------------------------------------
# MCPTool._format_result
# ---------------------------------------------------------------------------


@pytest.fixture
def mcp_tool() -> MCPTool:
    """Create a test MCPTool instance."""
    return MCPTool(
        name="test",
        description="Test tool",
        server_transport="stdio",
        server_command="echo",
        mcp_tool_name="test",
    )


def test_mcp_tool_format_result_text_joined(mcp_tool: MCPTool) -> None:
    """Text content is joined with newlines."""
    result = mcp_tool._format_result(
        SimpleNamespace(
            isError=False,
            content=[SimpleNamespace(text="Hello"), SimpleNamespace(text="World")],
        )
    )
    assert result == "Hello\nWorld"


def test_mcp_tool_format_result_error_flag(mcp_tool: MCPTool) -> None:
    """Error flag produces error message."""
    result = mcp_tool._format_result(
        SimpleNamespace(
            isError=True,
            content="Something went wrong",
        )
    )
    assert "MCP Tool Error" in result


def test_mcp_tool_format_result_binary_content(mcp_tool: MCPTool) -> None:
    """Binary content shows mime type placeholder."""
    result = mcp_tool._format_result(
        SimpleNamespace(
            isError=False,
            content=[SimpleNamespace(data=b"bytes", mimeType="image/png")],
        )
    )
    assert "[Binary data: image/png]" in result
