"""
Tests for the MCP client wrapper utilities.

These tests verify:
- MCPClient connection and tool operations
- MCPClientManager multi-server management
- Error handling
- Async utilities
"""

from unittest.mock import 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 TestMCPClientInit:
    """Tests for MCPClient initialization."""

    def test_init_basic(self):
        """Test basic initialization."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            from local_deep_research.mcp.client import MCPClient

            config = {
                "name": "test-server",
                "command": "python",
                "args": ["-m", "test"],
            }

            client = MCPClient(config)

            assert client.name == "test-server"
            assert client.config == config
            assert client.timeout == 60.0
            assert not client._connected

    def test_init_with_custom_timeout(self):
        """Test initialization with custom timeout."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            from local_deep_research.mcp.client import MCPClient

            config = {"name": "test", "command": "python"}
            client = MCPClient(config, timeout=120.0)

            assert client.timeout == 120.0

    def test_init_unnamed_server(self):
        """Test initialization without name uses default."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            from local_deep_research.mcp.client import MCPClient

            config = {"command": "python"}
            client = MCPClient(config)

            assert client.name == "unnamed"

    def test_init_mcp_not_available(self):
        """Test initialization raises error when MCP not installed."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", False):
            # Need to reload the module to get the error
            from local_deep_research.mcp.client import MCPClient, MCPClientError

            config = {"command": "python"}

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

            assert "not available" in str(exc_info.value)


class TestMCPClientError:
    """Tests for MCPClientError exception."""

    def test_error_inherits_from_exception(self):
        """Test MCPClientError is a proper exception."""
        from local_deep_research.mcp.client import MCPClientError

        with pytest.raises(MCPClientError):
            raise MCPClientError("Test error")

    def test_error_message(self):
        """Test error message is preserved."""
        from local_deep_research.mcp.client import MCPClientError

        error = MCPClientError("Custom error message")
        assert str(error) == "Custom error message"


class TestMCPClientManagerInit:
    """Tests for MCPClientManager initialization."""

    def test_init_with_configs(self):
        """Test initialization with server configs."""
        from local_deep_research.mcp.client import MCPClientManager

        configs = [
            {"name": "server1", "command": "command1"},
            {"name": "server2", "command": "command2"},
        ]

        manager = MCPClientManager(configs)

        assert manager.server_configs == configs
        assert len(manager._clients) == 0  # Not connected yet

    def test_init_empty_configs(self):
        """Test initialization with empty configs."""
        from local_deep_research.mcp.client import MCPClientManager

        manager = MCPClientManager([])

        assert manager.server_configs == []

    def test_get_connected_servers_empty(self):
        """Test getting connected servers when none connected."""
        from local_deep_research.mcp.client import MCPClientManager

        manager = MCPClientManager([])

        assert manager.get_connected_servers() == []


class TestRunAsync:
    """Tests for the run_async helper function."""

    def test_run_async_simple_coroutine(self):
        """Test running a simple coroutine."""
        from local_deep_research.mcp.client import run_async

        async def simple_coro():
            return "result"

        result = run_async(simple_coro())

        assert result == "result"

    def test_run_async_with_exception(self):
        """Test run_async propagates exceptions."""
        from local_deep_research.mcp.client import run_async

        async def error_coro():
            raise ValueError("Test error")

        with pytest.raises(ValueError) as exc_info:
            run_async(error_coro())

        assert "Test error" in str(exc_info.value)

    def test_run_async_from_async_context(self):
        """Test run_async works when called from within an async context.

        This tests the nested event loop path that uses ThreadPoolExecutor.
        """
        import asyncio
        from local_deep_research.mcp.client import run_async

        async def inner_coro():
            return "inner_result"

        async def outer_coro():
            # This simulates calling run_async from within an async context
            # which triggers the ThreadPoolExecutor path
            result = run_async(inner_coro())
            return result

        # Run the outer coroutine which will call run_async internally
        result = asyncio.run(outer_coro())
        assert result == "inner_result"

    def test_run_async_nested_with_exception(self):
        """Test run_async propagates exceptions from nested context."""
        import asyncio
        from local_deep_research.mcp.client import run_async

        async def inner_coro():
            raise ValueError("Nested error")

        async def outer_coro():
            return run_async(inner_coro())

        with pytest.raises(ValueError) as exc_info:
            asyncio.run(outer_coro())

        assert "Nested error" in str(exc_info.value)

    def test_run_async_timeout_in_nested_context(self):
        """Test run_async respects timeout parameter in nested async context.

        Note: Timeout only applies when calling from within an existing event loop,
        where ThreadPoolExecutor is used. In the simple case (no event loop),
        asyncio.run() is used which doesn't have timeout.
        """
        import asyncio
        from local_deep_research.mcp.client import run_async, MCPClientError

        async def slow_coro():
            await asyncio.sleep(10)
            return "never reached"

        async def outer_coro():
            # Call run_async from within an async context to trigger
            # the ThreadPoolExecutor path which respects timeout
            return run_async(slow_coro(), timeout=0.1)

        with pytest.raises(MCPClientError) as exc_info:
            asyncio.run(outer_coro())

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


class TestMCPClientNotConnected:
    """Tests for operations when client is not connected."""

    def test_list_tools_not_connected(self):
        """Test list_tools raises error when not connected."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            from local_deep_research.mcp.client import (
                MCPClient,
                MCPClientError,
                run_async,
            )

            config = {"name": "test", "command": "python"}
            client = MCPClient(config)

            async def test():
                return await client.list_tools()

            with pytest.raises(MCPClientError) as exc_info:
                run_async(test())

            assert "Not connected" in str(exc_info.value)

    def test_call_tool_not_connected(self):
        """Test call_tool raises error when not connected."""
        with patch("local_deep_research.mcp.client.MCP_AVAILABLE", True):
            from local_deep_research.mcp.client import (
                MCPClient,
                MCPClientError,
                run_async,
            )

            config = {"name": "test", "command": "python"}
            client = MCPClient(config)

            async def test():
                return await client.call_tool("test_tool", {})

            with pytest.raises(MCPClientError) as exc_info:
                run_async(test())

            assert "Not connected" in str(exc_info.value)


class TestMCPClientManagerOperations:
    """Tests for MCPClientManager operations."""

    def test_call_tool_server_not_connected(self):
        """Test calling tool on non-connected server raises error."""
        from local_deep_research.mcp.client import (
            MCPClientManager,
            MCPClientError,
            run_async,
        )

        manager = MCPClientManager([])

        async def test():
            return await manager.call_tool("unknown_server", "tool", {})

        with pytest.raises(MCPClientError) as exc_info:
            run_async(test())

        assert "not connected" in str(exc_info.value)


class TestModuleExports:
    """Tests for module exports."""

    def test_mcp_available_exported(self):
        """Test MCP_AVAILABLE is exported."""
        from local_deep_research.mcp.client import MCP_AVAILABLE

        assert isinstance(MCP_AVAILABLE, bool)

    def test_all_classes_exported(self):
        """Test all main classes are exported."""
        from local_deep_research.mcp.client import (
            MCPClient,
            MCPClientManager,
            MCPClientError,
            run_async,
        )

        assert MCPClient is not None
        assert MCPClientManager is not None
        assert MCPClientError is not None
        assert callable(run_async)
