"""Tests for error handling middleware."""

import logging
from unittest.mock import AsyncMock, MagicMock

import pytest
from mcp import McpError

from fastmcp.server.middleware.error_handling import (
    ErrorHandlingMiddleware,
    RetryMiddleware,
)
from fastmcp.server.middleware.middleware import MiddlewareContext
from fastmcp.utilities.tests import caplog_for_fastmcp


@pytest.fixture
def mock_context():
    """Create a mock middleware context."""
    context = MagicMock(spec=MiddlewareContext)
    context.method = "test_method"
    return context


@pytest.fixture
def mock_call_next():
    """Create a mock call_next function."""
    return AsyncMock(return_value="test_result")


class TestErrorHandlingMiddleware:
    """Test error handling middleware functionality."""

    def test_init_default(self):
        """Test default initialization."""
        middleware = ErrorHandlingMiddleware()
        assert middleware.logger.name == "fastmcp.errors"
        assert middleware.include_traceback is False
        assert middleware.error_callback is None
        assert middleware.transform_errors is True
        assert middleware.error_counts == {}

    def test_init_custom(self):
        """Test custom initialization."""
        logger = logging.getLogger("custom")
        callback = MagicMock()

        middleware = ErrorHandlingMiddleware(
            logger=logger,
            include_traceback=True,
            error_callback=callback,
            transform_errors=False,
        )
        assert middleware.logger is logger
        assert middleware.include_traceback is True
        assert middleware.error_callback is callback
        assert middleware.transform_errors is False

    def test_log_error_basic(self, mock_context, caplog):
        """Test basic error logging."""
        middleware = ErrorHandlingMiddleware()
        error = ValueError("test error")

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                middleware._log_error(error, mock_context)

        assert "Error in test_method: ValueError: test error" in caplog.text
        assert "ValueError:test_method" in middleware.error_counts
        assert middleware.error_counts["ValueError:test_method"] == 1

    def test_log_error_with_traceback(self, mock_context, caplog):
        """Test error logging with traceback."""
        middleware = ErrorHandlingMiddleware(include_traceback=True)
        error = ValueError("test error")

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                middleware._log_error(error, mock_context)

        assert "Error in test_method: ValueError: test error" in caplog.text
        # The traceback is added to the log message
        assert "Error in test_method: ValueError: test error" in caplog.text

    def test_log_error_with_callback(self, mock_context):
        """Test error logging with callback."""
        callback = MagicMock()
        middleware = ErrorHandlingMiddleware(error_callback=callback)
        error = ValueError("test error")

        middleware._log_error(error, mock_context)

        callback.assert_called_once_with(error, mock_context)

    def test_log_error_callback_exception(self, mock_context, caplog):
        """Test error logging when callback raises exception."""
        callback = MagicMock(side_effect=RuntimeError("callback error"))
        middleware = ErrorHandlingMiddleware(error_callback=callback)
        error = ValueError("test error")

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                middleware._log_error(error, mock_context)

        assert "Error in error callback: callback error" in caplog.text

    def test_transform_error_mcp_error(self):
        """Test that MCP errors are not transformed."""
        middleware = ErrorHandlingMiddleware()
        from mcp.types import ErrorData

        error = McpError(ErrorData(code=-32001, message="test error"))

        result = middleware._transform_error(error)

        assert result is error

    def test_transform_error_disabled(self):
        """Test error transformation when disabled."""
        middleware = ErrorHandlingMiddleware(transform_errors=False)
        error = ValueError("test error")

        result = middleware._transform_error(error)

        assert result is error

    def test_transform_error_value_error(self):
        """Test transforming ValueError."""
        middleware = ErrorHandlingMiddleware()
        error = ValueError("test error")

        result = middleware._transform_error(error)

        assert isinstance(result, McpError)
        assert result.error.code == -32602
        assert "Invalid params: test error" in result.error.message

    def test_transform_error_file_not_found(self):
        """Test transforming FileNotFoundError."""
        middleware = ErrorHandlingMiddleware()
        error = FileNotFoundError("test error")

        result = middleware._transform_error(error)

        assert isinstance(result, McpError)
        assert result.error.code == -32001
        assert "Resource not found: test error" in result.error.message

    def test_transform_error_permission_error(self):
        """Test transforming PermissionError."""
        middleware = ErrorHandlingMiddleware()
        error = PermissionError("test error")

        result = middleware._transform_error(error)

        assert isinstance(result, McpError)
        assert result.error.code == -32000
        assert "Permission denied: test error" in result.error.message

    def test_transform_error_timeout_error(self):
        """Test transforming TimeoutError."""
        middleware = ErrorHandlingMiddleware()
        error = TimeoutError("test error")

        result = middleware._transform_error(error)

        assert isinstance(result, McpError)
        assert result.error.code == -32000
        assert "Request timeout: test error" in result.error.message

    def test_transform_error_generic(self):
        """Test transforming generic error."""
        middleware = ErrorHandlingMiddleware()
        error = RuntimeError("test error")

        result = middleware._transform_error(error)

        assert isinstance(result, McpError)
        assert result.error.code == -32603
        assert "Internal error: test error" in result.error.message

    async def test_on_message_success(self, mock_context, mock_call_next):
        """Test successful message handling."""
        middleware = ErrorHandlingMiddleware()

        result = await middleware.on_message(mock_context, mock_call_next)

        assert result == "test_result"
        assert mock_call_next.called

    async def test_on_message_error_transform(self, mock_context, caplog):
        """Test error handling with transformation."""
        middleware = ErrorHandlingMiddleware()
        mock_call_next = AsyncMock(side_effect=ValueError("test error"))

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                with pytest.raises(McpError) as exc_info:
                    await middleware.on_message(mock_context, mock_call_next)

        assert isinstance(exc_info.value, McpError)
        assert exc_info.value.error.code == -32602
        assert "Invalid params: test error" in exc_info.value.error.message
        assert "Error in test_method: ValueError: test error" in caplog.text

    def test_get_error_stats(self, mock_context):
        """Test getting error statistics."""
        middleware = ErrorHandlingMiddleware()
        error1 = ValueError("error1")
        error2 = ValueError("error2")
        error3 = RuntimeError("error3")

        middleware._log_error(error1, mock_context)
        middleware._log_error(error2, mock_context)
        middleware._log_error(error3, mock_context)

        stats = middleware.get_error_stats()
        assert stats["ValueError:test_method"] == 2
        assert stats["RuntimeError:test_method"] == 1


class TestRetryMiddleware:
    """Test retry middleware functionality."""

    def test_init_default(self):
        """Test default initialization."""
        middleware = RetryMiddleware()
        assert middleware.max_retries == 3
        assert middleware.base_delay == 1.0
        assert middleware.max_delay == 60.0
        assert middleware.backoff_multiplier == 2.0
        assert middleware.retry_exceptions == (ConnectionError, TimeoutError)
        assert middleware.logger.name == "fastmcp.retry"

    def test_init_custom(self):
        """Test custom initialization."""
        logger = logging.getLogger("custom")
        middleware = RetryMiddleware(
            max_retries=5,
            base_delay=2.0,
            max_delay=120.0,
            backoff_multiplier=3.0,
            retry_exceptions=(ValueError, RuntimeError),
            logger=logger,
        )
        assert middleware.max_retries == 5
        assert middleware.base_delay == 2.0
        assert middleware.max_delay == 120.0
        assert middleware.backoff_multiplier == 3.0
        assert middleware.retry_exceptions == (ValueError, RuntimeError)
        assert middleware.logger is logger

    def test_should_retry_true(self):
        """Test retry decision for retryable errors."""
        middleware = RetryMiddleware()

        assert middleware._should_retry(ConnectionError()) is True
        assert middleware._should_retry(TimeoutError()) is True

    def test_should_retry_false(self):
        """Test retry decision for non-retryable errors."""
        middleware = RetryMiddleware()

        assert middleware._should_retry(ValueError()) is False
        assert middleware._should_retry(RuntimeError()) is False

    def test_calculate_delay(self):
        """Test delay calculation."""
        middleware = RetryMiddleware(
            base_delay=1.0, backoff_multiplier=2.0, max_delay=10.0
        )

        assert middleware._calculate_delay(0) == 1.0
        assert middleware._calculate_delay(1) == 2.0
        assert middleware._calculate_delay(2) == 4.0
        assert middleware._calculate_delay(3) == 8.0
        assert middleware._calculate_delay(4) == 10.0  # capped at max_delay

    async def test_on_request_success_first_try(self, mock_context, mock_call_next):
        """Test successful request on first try."""
        middleware = RetryMiddleware()

        result = await middleware.on_request(mock_context, mock_call_next)

        assert result == "test_result"
        assert mock_call_next.call_count == 1

    async def test_on_request_success_after_retries(self, mock_context, caplog):
        """Test successful request after retries."""
        middleware = RetryMiddleware(base_delay=0.01)  # Fast retry for testing

        # Fail first two attempts, succeed on third
        mock_call_next = AsyncMock(
            side_effect=[
                ConnectionError("connection failed"),
                ConnectionError("connection failed"),
                "test_result",
            ]
        )

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.WARNING):
                result = await middleware.on_request(mock_context, mock_call_next)

        assert result == "test_result"
        assert mock_call_next.call_count == 3
        assert "Retrying in" in caplog.text

    async def test_on_request_max_retries_exceeded(self, mock_context, caplog):
        """Test request failing after max retries."""
        middleware = RetryMiddleware(max_retries=2, base_delay=0.01)

        # Fail all attempts
        mock_call_next = AsyncMock(side_effect=ConnectionError("connection failed"))

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.WARNING):
                with pytest.raises(ConnectionError):
                    await middleware.on_request(mock_context, mock_call_next)

        assert mock_call_next.call_count == 3  # initial + 2 retries
        assert "Retrying in" in caplog.text

    async def test_on_request_non_retryable_error(self, mock_context):
        """Test non-retryable error is not retried."""
        middleware = RetryMiddleware()
        mock_call_next = AsyncMock(side_effect=ValueError("non-retryable"))

        with pytest.raises(ValueError):
            await middleware.on_request(mock_context, mock_call_next)

        assert mock_call_next.call_count == 1  # No retries


@pytest.fixture
def error_handling_server():
    """Create a FastMCP server specifically for error handling middleware tests."""
    from fastmcp import FastMCP

    mcp = FastMCP("ErrorHandlingTestServer")

    @mcp.tool
    def reliable_operation(data: str) -> str:
        """A reliable operation that always succeeds."""
        return f"Success: {data}"

    @mcp.tool
    def failing_operation(error_type: str = "value") -> str:
        """An operation that fails with different error types."""
        if error_type == "value":
            raise ValueError("Value error occurred")
        elif error_type == "file":
            raise FileNotFoundError("File not found")
        elif error_type == "permission":
            raise PermissionError("Permission denied")
        elif error_type == "timeout":
            raise TimeoutError("Operation timed out")
        elif error_type == "generic":
            raise RuntimeError("Generic runtime error")
        else:
            return "Operation completed"

    @mcp.tool
    def intermittent_operation(fail_rate: float = 0.5) -> str:
        """An operation that fails intermittently."""
        import random

        if random.random() < fail_rate:
            raise ConnectionError("Random connection failure")
        return "Operation succeeded"

    @mcp.tool
    def retryable_operation(attempt_count: int = 0) -> str:
        """An operation that succeeds after a few attempts."""
        # This is a simple way to simulate retry behavior
        # In a real scenario, you might use external state
        if attempt_count < 2:
            raise ConnectionError("Temporary connection error")
        return "Operation succeeded after retries"

    return mcp


class TestErrorHandlingMiddlewareIntegration:
    """Integration tests for error handling middleware with real FastMCP server."""

    async def test_error_handling_middleware_logs_real_errors(
        self, error_handling_server, caplog
    ):
        """Test that error handling middleware logs real errors from tools."""
        from fastmcp.client import Client

        error_handling_server.add_middleware(ErrorHandlingMiddleware())

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                async with Client(error_handling_server) as client:
                    # Test different types of errors
                    with pytest.raises(Exception):
                        await client.call_tool(
                            "failing_operation", {"error_type": "value"}
                        )

                    with pytest.raises(Exception):
                        await client.call_tool(
                            "failing_operation", {"error_type": "file"}
                        )

        log_text = caplog.text

        # Should have error logs for both failures
        assert "Error in tools/call: ToolError:" in log_text
        # Should have captured both error instances
        error_count = log_text.count("Error in tools/call:")
        assert error_count == 2

    async def test_error_handling_middleware_tracks_error_statistics(
        self, error_handling_server
    ):
        """Test that error handling middleware accurately tracks error statistics."""
        from fastmcp.client import Client

        error_middleware = ErrorHandlingMiddleware()
        error_handling_server.add_middleware(error_middleware)

        async with Client(error_handling_server) as client:
            # Generate different types of errors
            for _ in range(3):
                with pytest.raises(Exception):
                    await client.call_tool("failing_operation", {"error_type": "value"})

            for _ in range(2):
                with pytest.raises(Exception):
                    await client.call_tool("failing_operation", {"error_type": "file"})

            # Try some intermittent operations (some may succeed)
            for _ in range(5):
                try:
                    await client.call_tool("intermittent_operation", {"fail_rate": 0.8})
                except Exception:
                    pass  # Expected failures

        # Check error statistics
        stats = error_middleware.get_error_stats()

        # Should have tracked the ToolError wrapper
        assert "ToolError:tools/call" in stats
        assert stats["ToolError:tools/call"] >= 5  # At least the 5 deliberate failures

    async def test_error_handling_middleware_with_success_and_failure(
        self, error_handling_server, caplog
    ):
        """Test error handling middleware with mix of successful and failed operations."""
        from fastmcp.client import Client

        error_handling_server.add_middleware(ErrorHandlingMiddleware())

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                async with Client(error_handling_server) as client:
                    # Successful operation (should not generate error logs)
                    await client.call_tool("reliable_operation", {"data": "test"})

                    # Failed operation (should generate error log)
                    with pytest.raises(Exception):
                        await client.call_tool(
                            "failing_operation", {"error_type": "value"}
                        )

                    # Another successful operation
                    await client.call_tool("reliable_operation", {"data": "test2"})

        log_text = caplog.text

        # Should only have one error log (for the failed operation)
        error_count = log_text.count("Error in tools/call:")
        assert error_count == 1

    async def test_error_handling_middleware_custom_callback(
        self, error_handling_server
    ):
        """Test error handling middleware with custom error callback."""
        from fastmcp.client import Client

        captured_errors = []

        def error_callback(error, context):
            captured_errors.append(
                {
                    "error_type": type(error).__name__,
                    "message": str(error),
                    "method": context.method,
                }
            )

        error_handling_server.add_middleware(
            ErrorHandlingMiddleware(error_callback=error_callback)
        )

        async with Client(error_handling_server) as client:
            # Generate some errors
            with pytest.raises(Exception):
                await client.call_tool("failing_operation", {"error_type": "value"})

            with pytest.raises(Exception):
                await client.call_tool("failing_operation", {"error_type": "timeout"})

        # Check that callback was called
        assert len(captured_errors) == 2
        assert captured_errors[0]["error_type"] == "ToolError"
        assert captured_errors[1]["error_type"] == "ToolError"
        assert all(error["method"] == "tools/call" for error in captured_errors)

    async def test_error_handling_middleware_transform_errors(
        self, error_handling_server
    ):
        """Test error transformation functionality."""
        from fastmcp.client import Client

        error_handling_server.add_middleware(
            ErrorHandlingMiddleware(transform_errors=True)
        )

        async with Client(error_handling_server) as client:
            # All errors should still be raised, but potentially transformed
            with pytest.raises(Exception) as exc_info:
                await client.call_tool("failing_operation", {"error_type": "value"})

            # Error should still exist (may be wrapped by FastMCP)
            assert exc_info.value is not None


class TestRetryMiddlewareIntegration:
    """Integration tests for retry middleware with real FastMCP server."""

    async def test_retry_middleware_with_transient_failures(
        self, error_handling_server, caplog
    ):
        """Test retry middleware with operations that have transient failures."""
        from fastmcp.client import Client

        # Configure retry middleware to retry connection errors
        error_handling_server.add_middleware(
            RetryMiddleware(
                max_retries=3,
                base_delay=0.01,  # Very short delay for testing
                retry_exceptions=(ConnectionError,),
            )
        )

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.WARNING):
                async with Client(error_handling_server) as client:
                    # This operation fails intermittently - try several times
                    success_count = 0
                    for _ in range(5):
                        try:
                            await client.call_tool(
                                "intermittent_operation", {"fail_rate": 0.7}
                            )
                            success_count += 1
                        except Exception:
                            pass  # Some failures expected even with retries

        # Should have some retry log messages
        # Note: Retry logs might not appear if the underlying errors are wrapped by FastMCP
        # The key is that some operations should succeed due to retries

    async def test_retry_middleware_with_permanent_failures(
        self, error_handling_server
    ):
        """Test that retry middleware doesn't retry non-retryable errors."""
        from fastmcp.client import Client

        # Configure retry middleware for connection errors only
        error_handling_server.add_middleware(
            RetryMiddleware(
                max_retries=3, base_delay=0.01, retry_exceptions=(ConnectionError,)
            )
        )

        async with Client(error_handling_server) as client:
            # Value errors should not be retried
            with pytest.raises(Exception):
                await client.call_tool("failing_operation", {"error_type": "value"})

            # Should fail immediately without retries

    async def test_combined_error_handling_and_retry_middleware(
        self, error_handling_server, caplog
    ):
        """Test error handling and retry middleware working together."""
        from fastmcp.client import Client

        # Add both middleware
        error_handling_server.add_middleware(ErrorHandlingMiddleware())
        error_handling_server.add_middleware(
            RetryMiddleware(
                max_retries=2, base_delay=0.01, retry_exceptions=(ConnectionError,)
            )
        )

        with caplog_for_fastmcp(caplog):
            with caplog.at_level(logging.ERROR):
                async with Client(error_handling_server) as client:
                    # Try intermittent operation
                    try:
                        await client.call_tool(
                            "intermittent_operation", {"fail_rate": 0.9}
                        )
                    except Exception:
                        pass  # May still fail even with retries

                    # Try permanent failure
                    with pytest.raises(Exception):
                        await client.call_tool(
                            "failing_operation", {"error_type": "value"}
                        )

        log_text = caplog.text

        # Should have error logs from error handling middleware
        assert "Error in tools/call:" in log_text
