"""Tests for server error handling."""

import logging
from collections.abc import Generator
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mcp_server_neurolorap.server import run_dev_mode

# Disable logging for tests
logging.getLogger("mcp_server_neurolorap.server").setLevel(logging.CRITICAL)


class MockTerminal:
    """Mock terminal for testing."""

    def __init__(self) -> None:
        """Initialize mock terminal."""
        self.parse_request_calls = 0
        self.handle_command_calls = 0

    def parse_request(self, line: str) -> dict[str, Any] | None:
        """Mock parse_request method."""
        self.parse_request_calls += 1
        if line == "unknown_command":
            return {
                "jsonrpc": "2.0",
                "method": "unknown_command",
                "params": [],
                "id": 1,
            }
        elif line == "exit":
            return {
                "jsonrpc": "2.0",
                "method": "exit",
                "params": [],
                "id": 2,
            }
        return None

    async def handle_command(self, request: dict[str, Any]) -> dict[str, Any]:
        """Mock handle_command method."""
        self.handle_command_calls += 1
        if request["method"] == "unknown_command":
            return {
                "error": {"message": "Method 'unknown_command' not found"},
                "id": request["id"],
            }
        elif request["method"] == "exit":
            return {"result": "Goodbye!", "id": request["id"]}
        return {"error": {"message": "Invalid request"}, "id": request["id"]}


class ToolMock(AsyncMock):
    """Custom AsyncMock that matches the expected tool callable type."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self._side_effect: Exception | None = None

    def set_side_effect(self, effect: Exception | None) -> None:
        """Set side effect for the mock."""
        self._side_effect = effect

    async def __call__(self, *args: Any, **kwargs: Any) -> str:
        """Mock tool call."""
        try:
            if self._side_effect:
                raise self._side_effect
            return "Success"
        except Exception:
            if kwargs.get("tool_name") == "project_structure_reporter":
                return "Error generating report"
            return "No files found to process or error occurred"


@pytest.fixture
def mock_fastmcp() -> Generator[MagicMock, None, None]:
    """Mock FastMCP server."""
    with patch("mcp_server_neurolorap.server.FastMCP") as mock:
        mock_server = MagicMock()
        mock_server.name = "neurolorap"
        mock_server.tools = {
            "project_structure_reporter": ToolMock(),
            "code_collector": ToolMock(),
        }
        mock_server.tool_called = False
        mock.return_value = mock_server
        yield mock_server


@pytest.mark.asyncio
async def test_project_structure_reporter_error_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mock_fastmcp: MagicMock
) -> None:
    """Test error handling in project_structure_reporter tool."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", str(tmp_path))

    # Test with invalid ignore patterns
    tool = mock_fastmcp.tools["project_structure_reporter"]
    tool.set_side_effect(ValueError("Invalid pattern"))
    result = await tool(
        tool_name="project_structure_reporter",
        output_filename="test.md",
        ignore_patterns=["["],
    )
    assert "Error generating report" in result

    # Test with file system error
    tool = mock_fastmcp.tools["project_structure_reporter"]
    tool.set_side_effect(OSError("Permission denied"))
    result = await tool(tool_name="project_structure_reporter")
    assert "Error generating report" in result

    # Test with analysis error
    tool = mock_fastmcp.tools["project_structure_reporter"]
    tool.set_side_effect(ValueError("Analysis failed"))
    result = await tool(tool_name="project_structure_reporter")
    assert "Error generating report" in result

    # Test with report generation error
    tool = mock_fastmcp.tools["project_structure_reporter"]
    tool.set_side_effect(ValueError("Report generation failed"))
    result = await tool(tool_name="project_structure_reporter")
    assert "Error generating report" in result


@pytest.mark.asyncio
async def test_code_collector_error_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mock_fastmcp: MagicMock
) -> None:
    """Test error handling in code_collector tool."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", str(tmp_path))

    # Test with invalid input path
    tool = mock_fastmcp.tools["code_collector"]
    tool.set_side_effect(ValueError("Invalid path"))
    result = await tool(
        tool_name="code_collector",
        input_path="/nonexistent/path",
        title="Test",
    )
    assert "No files found to process or error occurred" in result

    # Test with file system error
    tool = mock_fastmcp.tools["code_collector"]
    tool.set_side_effect(OSError("Permission denied"))
    result = await tool(tool_name="code_collector")
    assert "No files found to process or error occurred" in result

    # Test with collection error
    tool = mock_fastmcp.tools["code_collector"]
    tool.set_side_effect(ValueError("Collection failed"))
    result = await tool(tool_name="code_collector")
    assert "No files found to process or error occurred" in result

    # Test with unexpected error
    tool = mock_fastmcp.tools["code_collector"]
    tool.set_side_effect(Exception("Unexpected error"))
    result = await tool(tool_name="code_collector")
    assert "No files found to process or error occurred" in result


@pytest.mark.asyncio
async def test_run_dev_mode_value_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test error handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock print function to capture output
    prints: list[str] = []

    def print_mock(x: object) -> None:
        prints.append(str(x))

    monkeypatch.setattr("builtins.print", print_mock)

    # Mock input to raise ValueError
    input_mock = MagicMock(side_effect=[ValueError("Invalid input"), "exit"])
    monkeypatch.setattr("builtins.input", input_mock)

    # Run dev mode
    await run_dev_mode()

    # Verify error handling
    assert any("Value error: Invalid input" in msg for msg in prints)
    assert any("Exiting developer mode" in msg for msg in prints)


@pytest.mark.asyncio
async def test_run_dev_mode_type_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test type error handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock print function to capture output
    prints: list[str] = []

    def print_mock(x: object) -> None:
        prints.append(str(x))

    monkeypatch.setattr("builtins.print", print_mock)

    # Mock input to raise TypeError
    input_mock = MagicMock(side_effect=[TypeError("Invalid type"), "exit"])
    monkeypatch.setattr("builtins.input", input_mock)

    # Run dev mode
    await run_dev_mode()

    # Verify error handling
    assert any("Type error: Invalid type" in msg for msg in prints)
    assert any("Exiting developer mode" in msg for msg in prints)


@pytest.mark.asyncio
async def test_run_dev_mode_empty_input(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test empty input handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock print function to capture output
    prints: list[str] = []

    def print_mock(x: object) -> None:
        prints.append(str(x))

    monkeypatch.setattr("builtins.print", print_mock)

    # Mock input to return empty string then exit
    input_mock = MagicMock(side_effect=["", "exit"])
    monkeypatch.setattr("builtins.input", input_mock)

    # Run dev mode
    await run_dev_mode()

    # Verify error handling
    assert not any("Invalid command format" in msg for msg in prints)
    assert any("Exiting developer mode" in msg for msg in prints)


@pytest.mark.asyncio
async def test_run_dev_mode_invalid_command(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test invalid command format handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock print function to capture output
    prints: list[str] = []

    def print_mock(x: object) -> None:
        prints.append(str(x))

    monkeypatch.setattr("builtins.print", print_mock)

    # Mock terminal to return None for parse_request
    terminal = MockTerminal()
    with patch(
        "mcp_server_neurolorap.server.JsonRpcTerminal",
        return_value=terminal,
    ):
        # Mock input with invalid command then exit
        input_mock = MagicMock(side_effect=["invalid command", "exit"])
        monkeypatch.setattr("builtins.input", input_mock)

        # Run dev mode
        await run_dev_mode()

        # Verify error handling
        assert any("Invalid command format" in msg for msg in prints)
        assert any("Exiting developer mode" in msg for msg in prints)


@pytest.mark.asyncio
async def test_run_dev_mode_unknown_command(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test unknown command handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock JsonRpcTerminal class
    with patch(
        "mcp_server_neurolorap.server.JsonRpcTerminal"
    ) as mock_terminal_class:
        terminal_instance = MockTerminal()
        mock_terminal_class.return_value = terminal_instance

        # Mock input to immediately exit
        input_mock = MagicMock(side_effect=["unknown_command", "exit"])
        monkeypatch.setattr("builtins.input", input_mock)

        # Mock print function to capture output
        prints: list[str] = []

        def print_mock(x: object) -> None:
            prints.append(str(x))

        monkeypatch.setattr("builtins.print", print_mock)

        # Run dev mode
        await run_dev_mode()

        # Verify error handling
        error_msg = "Error: Method 'unknown_command' not found"
        has_error = any(error_msg in msg for msg in prints)
        has_exit = any("Exiting developer mode" in msg for msg in prints)

        assert has_error, f"Expected '{error_msg}' in output"
        assert has_exit, "Expected 'Exiting developer mode' message"


@pytest.mark.asyncio
async def test_run_dev_mode_keyboard_interrupt(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test keyboard interrupt handling in run_dev_mode."""
    monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")

    # Mock input function to raise KeyboardInterrupt
    input_mock = MagicMock(side_effect=KeyboardInterrupt)
    monkeypatch.setattr("builtins.input", input_mock)

    # Mock print function to capture output
    prints: list[str] = []

    def print_mock(x: object) -> None:
        prints.append(str(x))

    monkeypatch.setattr("builtins.print", print_mock)

    # Run dev mode
    await run_dev_mode()

    # Verify error handling
    assert any("Exiting developer mode" in msg for msg in prints)
