"""Fast mock-based tests for MCP client logic.

The subprocess-based integration tests (spawning a real echo server)
now live at tests/performance/mcp/test_mcp_client_server_live.py —
co-located with the echo_server.py they rely on.
"""

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# Skip all tests if MCP is not available
try:
    import mcp  # noqa: F401

    MCP_AVAILABLE = True
except ImportError:
    MCP_AVAILABLE = False

pytestmark = pytest.mark.skipif(
    not MCP_AVAILABLE, reason="MCP package not installed"
)


class TestMCPClientWithMockedSession:
    """Tests for MCPClient using mocked session for fast, reliable testing."""

    @pytest.fixture
    def mock_tool_result(self):
        """Create a mock tool result."""

        def _make_result(text_content, is_error=False):
            mock_item = MagicMock()
            mock_item.text = text_content
            mock_result = MagicMock()
            mock_result.isError = is_error
            mock_result.content = [mock_item]
            return mock_result

        return _make_result

    @pytest.fixture
    def mock_list_tools_result(self):
        """Create a mock list tools result."""
        mock_tool1 = MagicMock()
        mock_tool1.name = "echo"
        mock_tool1.description = "Echo a message"
        mock_tool1.inputSchema = {
            "type": "object",
            "properties": {"message": {"type": "string"}},
        }

        mock_tool2 = MagicMock()
        mock_tool2.name = "add_numbers"
        mock_tool2.description = "Add two numbers"
        mock_tool2.inputSchema = {
            "type": "object",
            "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
        }

        mock_result = MagicMock()
        mock_result.tools = [mock_tool1, mock_tool2]
        return mock_result

    @pytest.mark.asyncio
    async def test_list_tools_with_mock(self, mock_list_tools_result):
        """Test list_tools returns properly formatted tool list."""
        from local_deep_research.mcp.client import MCPClient

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {"name": "mock-server", "command": "python"}
            client = MCPClient(config)

            # Set up mocked connected state
            client._connected = True
            client._session = MagicMock()
            client._session.list_tools = AsyncMock(
                return_value=mock_list_tools_result
            )

            tools = await client.list_tools()

            assert len(tools) == 2
            assert tools[0]["name"] == "echo"
            assert tools[0]["description"] == "Echo a message"
            assert tools[1]["name"] == "add_numbers"

    @pytest.mark.asyncio
    async def test_call_tool_success_with_mock(self, mock_tool_result):
        """Test call_tool returns success result."""
        from local_deep_research.mcp.client import MCPClient

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {"name": "mock-server", "command": "python"}
            client = MCPClient(config)

            client._connected = True
            client._session = MagicMock()
            client._session.call_tool = AsyncMock(
                return_value=mock_tool_result(
                    '{"status": "success", "message": "Hello, World!"}'
                )
            )

            result = await client.call_tool(
                "echo", {"message": "Hello, World!"}
            )

            assert result["status"] == "success"
            assert "Hello, World!" in result["content"]

    @pytest.mark.asyncio
    async def test_call_tool_error_result_with_mock(self, mock_tool_result):
        """Test call_tool handles error results from server."""
        from local_deep_research.mcp.client import MCPClient

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {"name": "mock-server", "command": "python"}
            client = MCPClient(config)

            client._connected = True
            client._session = MagicMock()
            client._session.call_tool = AsyncMock(
                return_value=mock_tool_result(
                    "Tool execution failed", is_error=True
                )
            )

            result = await client.call_tool("failing_tool", {})

            assert result["status"] == "error"

    @pytest.mark.asyncio
    async def test_call_tool_timeout_with_mock(self):
        """Test call_tool raises error on timeout."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {"name": "mock-server", "command": "python"}
            client = MCPClient(config, timeout=0.1)

            client._connected = True
            client._session = MagicMock()

            async def slow_call(*args, **kwargs):
                await asyncio.sleep(1.0)

            client._session.call_tool = slow_call

            with pytest.raises(MCPClientError) as exc_info:
                await client.call_tool("slow_tool", {})

            assert "timed out" in str(exc_info.value).lower()

    @pytest.mark.asyncio
    async def test_multiple_sequential_calls_with_mock(self, mock_tool_result):
        """Test multiple sequential tool calls work correctly."""
        from local_deep_research.mcp.client import MCPClient

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {"name": "mock-server", "command": "python"}
            client = MCPClient(config)

            client._connected = True
            client._session = MagicMock()

            call_count = 0

            async def mock_call_tool(name, args):
                nonlocal call_count
                call_count += 1
                return mock_tool_result(f"Result {call_count}")

            client._session.call_tool = mock_call_tool

            result1 = await client.call_tool("tool1", {})
            result2 = await client.call_tool("tool2", {})
            result3 = await client.call_tool("tool3", {})

            assert "Result 1" in result1["content"]
            assert "Result 2" in result2["content"]
            assert "Result 3" in result3["content"]
            assert call_count == 3


class TestMCPClientValidation:
    """Tests for client configuration validation with real commands."""

    def test_allowed_commands_includes_python(self):
        """Test that python is in allowed commands."""
        from local_deep_research.mcp.client import ALLOWED_COMMANDS

        assert "python" in ALLOWED_COMMANDS
        assert "python3" in ALLOWED_COMMANDS

    def test_disallowed_command_raises_error(self):
        """Test that disallowed commands raise MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {
                "name": "malicious",
                "command": "rm",  # Not in allowed list
                "args": ["-rf", "/"],
            }

            with pytest.raises(MCPClientError) as exc_info:
                MCPClient(config)

            assert "not in allowed list" in str(exc_info.value)

    def test_missing_command_raises_error(self):
        """Test that missing command raises MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {
                "name": "no-command",
                # Missing 'command' key
            }

            with pytest.raises(MCPClientError) as exc_info:
                MCPClient(config)

            assert "missing" in str(exc_info.value).lower()

    def test_invalid_args_type_raises_error(self):
        """Test that non-list args raises MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {
                "name": "bad-args",
                "command": "python",
                "args": "not-a-list",  # Should be a list
            }

            with pytest.raises(MCPClientError) as exc_info:
                MCPClient(config)

            assert "must be a list" in str(exc_info.value)

    def test_invalid_env_type_raises_error(self):
        """Test that non-dict env raises MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            config = {
                "name": "bad-env",
                "command": "python",
                "env": ["not", "a", "dict"],  # Should be a dict
            }

            with pytest.raises(MCPClientError) as exc_info:
                MCPClient(config)

            assert "must be a dictionary" in str(exc_info.value)
