import json
from unittest.mock import AsyncMock, Mock

import anyio
import mcp.types as types
import pytest

from minimcp.responder import Responder
from minimcp.time_limiter import TimeLimiter
from minimcp.types import Message

pytestmark = pytest.mark.anyio


@pytest.fixture(autouse=True)
async def timeout_5s():
    """Fail test if it takes longer than 5 seconds."""
    with anyio.fail_after(5):
        yield


class TestResponder:
    """Test suite for Responder class."""

    @pytest.fixture
    def mock_send(self) -> AsyncMock:
        """Create a mock send function."""
        return AsyncMock()

    @pytest.fixture
    def mock_time_limiter(self) -> Mock:
        """Create a mock TimeLimiter."""
        mock_limiter = Mock(spec=TimeLimiter)
        mock_limiter.reset = Mock()
        return mock_limiter

    @pytest.fixture
    def valid_request_message(self) -> Message:
        """Create a valid request message with progress token."""
        return json.dumps(
            {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {"name": "test_tool", "arguments": {}, "_meta": {"progressToken": "test-progress-token"}},
            }
        )

    @pytest.fixture
    def request_without_progress_token(self) -> Message:
        """Create a request message without progress token."""
        return json.dumps(
            {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "test_tool", "arguments": {}}}
        )

    @pytest.fixture
    def responder(self, valid_request_message: Message, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Create a Responder instance for testing."""
        return Responder(valid_request_message, mock_send, mock_time_limiter)

    @pytest.fixture
    def responder_no_token(
        self, request_without_progress_token: Message, mock_send: AsyncMock, mock_time_limiter: Mock
    ):
        """Create a Responder instance without progress token."""
        return Responder(request_without_progress_token, mock_send, mock_time_limiter)

    @pytest.fixture
    def responder_with_token(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Create a Responder instance with a mock progress token."""
        responder = Responder("dummy_request", mock_send, mock_time_limiter)
        responder._progress_token = "test-progress-token"
        return responder

    async def test_init_basic(self, valid_request_message: Message, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test basic Responder initialization."""
        responder = Responder(valid_request_message, mock_send, mock_time_limiter)

        assert responder._request == valid_request_message
        assert responder._send is mock_send
        assert responder._time_limiter is mock_time_limiter
        # Progress token extraction may fail due to MCP validation, which is acceptable
        # The important thing is that the responder is created successfully

    async def test_init_without_progress_token(
        self, request_without_progress_token: Message, mock_send: AsyncMock, mock_time_limiter: Mock
    ):
        """Test Responder initialization without progress token."""
        responder = Responder(request_without_progress_token, mock_send, mock_time_limiter)

        assert responder._request == request_without_progress_token
        assert responder._send is mock_send
        assert responder._time_limiter is mock_time_limiter
        assert responder._get_progress_token() is None

    async def test_get_progress_token_valid_request(self):
        """Test extracting progress token from valid request."""
        # Create a responder with a mocked progress token
        mock_send = AsyncMock()
        mock_time_limiter = Mock(spec=TimeLimiter)
        mock_time_limiter.reset = Mock()

        # Create responder and manually set progress token for testing
        responder = Responder("dummy_request", mock_send, mock_time_limiter)
        responder._progress_token = "test-progress-token"

        assert responder._progress_token == "test-progress-token"

    async def test_get_progress_token_no_token(self, responder_no_token: Responder):
        """Test extracting progress token when none exists."""
        assert responder_no_token._get_progress_token() is None

    async def test_get_progress_token_invalid_json(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction with invalid JSON."""
        invalid_json = '{"invalid": json}'
        responder = Responder(invalid_json, mock_send, mock_time_limiter)

        assert responder._get_progress_token() is None

    async def test_get_progress_token_invalid_structure(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction with invalid message structure."""
        invalid_structure = json.dumps({"not": "a valid request"})
        responder = Responder(invalid_structure, mock_send, mock_time_limiter)

        assert responder._get_progress_token() is None

    async def test_get_progress_token_missing_meta(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction when meta is missing."""
        no_meta = json.dumps(
            {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "test_tool", "arguments": {}}}
        )
        responder = Responder(no_meta, mock_send, mock_time_limiter)

        assert responder._get_progress_token() is None

    async def test_get_progress_token_missing_progress_token(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction when progressToken is missing from meta."""
        no_progress_token = json.dumps(
            {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {"name": "test_tool", "arguments": {}, "meta": {"other_field": "value"}},
            }
        )
        responder = Responder(no_progress_token, mock_send, mock_time_limiter)

        assert responder._get_progress_token() is None

    async def test_report_progress_with_token(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test reporting progress when progress token is available."""
        # Create responder with mock token
        responder = Responder("dummy_request", mock_send, mock_time_limiter)
        responder._progress_token = "test-progress-token"

        progress = 50.0
        total = 100.0
        message = "Processing..."

        result = await responder.report_progress(progress, total, message)

        assert result == "test-progress-token"
        mock_send.assert_called_once()
        mock_time_limiter.reset.assert_called_once()

        # Verify the notification structure
        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["jsonrpc"] == "2.0"
        assert notification_dict["method"] == "notifications/progress"
        assert notification_dict["params"]["progressToken"] == "test-progress-token"
        assert notification_dict["params"]["progress"] == progress
        assert notification_dict["params"]["total"] == total
        assert notification_dict["params"]["message"] == message

    async def test_report_progress_without_token(
        self, responder_no_token: Responder, mock_send: AsyncMock, mock_time_limiter: Mock
    ):
        """Test reporting progress when no progress token is available."""
        progress = 75.0

        result = await responder_no_token.report_progress(progress)

        assert result is None
        mock_send.assert_not_called()
        mock_time_limiter.reset.assert_not_called()

    async def test_report_progress_minimal_params(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test reporting progress with minimal parameters."""
        # Create responder with mock token
        responder = Responder("dummy_request", mock_send, mock_time_limiter)
        responder._progress_token = "test-progress-token"

        progress = 25.0

        result = await responder.report_progress(progress)

        assert result == "test-progress-token"
        mock_send.assert_called_once()

        # Verify the notification structure
        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == progress
        # Note: None values may not be included in JSON serialization
        assert notification_dict["params"].get("total") is None
        assert notification_dict["params"].get("message") is None

    async def test_report_progress_with_total_only(self, responder_with_token: Responder, mock_send: AsyncMock):
        """Test reporting progress with total but no message."""
        progress = 30.0
        total = 150.0

        result = await responder_with_token.report_progress(progress, total=total)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == progress
        assert notification_dict["params"]["total"] == total
        assert notification_dict["params"].get("message") is None

    async def test_report_progress_with_message_only(self, responder_with_token: Responder, mock_send: AsyncMock):
        """Test reporting progress with message but no total."""
        progress = 80.0
        message = "Almost done"

        result = await responder_with_token.report_progress(progress, message=message)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == progress
        assert notification_dict["params"].get("total") is None
        assert notification_dict["params"]["message"] == message

    async def test_report_progress_zero_progress(self, responder: Responder, mock_send: AsyncMock):
        """Test reporting zero progress."""
        progress = 0.0

        result = await responder.report_progress(progress)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == 0.0

    async def test_report_progress_negative_progress(self, responder: Responder, mock_send: AsyncMock):
        """Test reporting negative progress."""
        progress = -10.0

        result = await responder.report_progress(progress)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == -10.0

    async def test_report_progress_large_numbers(self, responder: Responder, mock_send: AsyncMock):
        """Test reporting progress with large numbers."""
        progress = 1000000.0
        total = 2000000.0

        result = await responder.report_progress(progress, total)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["progress"] == progress
        assert notification_dict["params"]["total"] == total

    async def test_send_notification_basic(self, responder: Responder, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test sending a basic notification."""
        # Create a test notification
        notification = types.ServerNotification(
            types.ProgressNotification(
                method="notifications/progress",
                params=types.ProgressNotificationParams(
                    progressToken="test-token", progress=50.0, total=100.0, message="Test message"
                ),
            )
        )

        await responder.send_notification(notification)

        mock_send.assert_called_once()
        mock_time_limiter.reset.assert_called_once()

        # Verify the sent message
        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["jsonrpc"] == "2.0"
        assert notification_dict["method"] == "notifications/progress"
        assert "params" in notification_dict

    async def test_send_notification_different_types(self, responder: Responder, mock_send: AsyncMock):
        """Test sending different types of notifications."""
        # Test with a different notification type
        notification = types.ServerNotification(
            types.CancelledNotification(
                method="notifications/cancelled",
                params=types.CancelledNotificationParams(requestId="test-request-id", reason="User cancelled"),
            )
        )

        await responder.send_notification(notification)

        mock_send.assert_called_once()

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["method"] == "notifications/cancelled"
        assert notification_dict["params"]["requestId"] == "test-request-id"

    async def test_send_notification_resets_timer(self, responder: Responder, mock_time_limiter: Mock):
        """Test that sending notification resets the time limiter."""
        notification = types.ServerNotification(
            types.ProgressNotification(
                method="notifications/progress",
                params=types.ProgressNotificationParams(progressToken="test-token", progress=25.0),
            )
        )

        await responder.send_notification(notification)

        mock_time_limiter.reset.assert_called_once()

    async def test_send_notification_calls_send_function(self, responder: Responder, mock_send: AsyncMock):
        """Test that send_notification calls the send function."""
        notification = types.ServerNotification(
            types.ProgressNotification(
                method="notifications/progress",
                params=types.ProgressNotificationParams(progressToken="test-token", progress=100.0),
            )
        )

        await responder.send_notification(notification)

        mock_send.assert_called_once()
        # Verify it's called with a string (JSON)
        call_args = mock_send.call_args[0][0]
        assert isinstance(call_args, str)

    async def test_multiple_progress_reports(self, responder: Responder, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test multiple progress reports."""
        progress_values = [10.0, 25.0, 50.0, 75.0, 100.0]

        for progress in progress_values:
            result = await responder.report_progress(progress)
            assert result == "test-progress-token"

        assert mock_send.call_count == len(progress_values)
        assert mock_time_limiter.reset.call_count == len(progress_values)

    async def test_progress_report_with_unicode_message(self, responder: Responder, mock_send: AsyncMock):
        """Test progress report with Unicode characters in message."""
        progress = 50.0
        message = "Processing 文档... 🚀"

        result = await responder.report_progress(progress, message=message)

        assert result == "test-progress-token"

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        assert notification_dict["params"]["message"] == message

    async def test_send_function_exception_propagation(self, responder: Responder, mock_time_limiter: Mock):
        """Test that exceptions from send function are propagated."""

        # Create a send function that raises an exception
        async def failing_send(message: str):
            raise ValueError("Send failed")

        responder._send = failing_send

        with pytest.raises(ValueError, match="Send failed"):
            await responder.report_progress(50.0)

        # Timer should still be reset even if send fails
        mock_time_limiter.reset.assert_called_once()

    async def test_progress_token_extraction_edge_cases(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction with various edge cases."""
        # Test with nested structure
        nested_request = json.dumps(
            {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                    "name": "test_tool",
                    "arguments": {},
                    "meta": {"progressToken": {"nested": "should-not-work"}},
                },
            }
        )

        responder = Responder(nested_request, mock_send, mock_time_limiter)
        # Should handle gracefully and return None
        result = responder._get_progress_token()
        assert result is None or isinstance(result, dict)

    async def test_progress_token_extraction_with_null_values(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction with null values."""
        null_token_request = json.dumps(
            {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {"name": "test_tool", "arguments": {}, "meta": {"progressToken": None}},
            }
        )

        responder = Responder(null_token_request, mock_send, mock_time_limiter)
        assert responder._get_progress_token() is None

    async def test_concurrent_progress_reports(self, responder: Responder, mock_time_limiter: Mock):
        """Test concurrent progress reports."""
        # Create a mock send that tracks calls
        call_count = 0

        async def counting_send(message: str):
            nonlocal call_count
            call_count += 1
            await anyio.sleep(0.01)  # Simulate some async work

        responder._send = counting_send

        # Send multiple progress reports concurrently
        results: list[str] = []

        async def send_progress(i: int):
            result: str = str(await responder.report_progress(i * 10.0, message=f"Step {i}"))
            results.append(result)

        async with anyio.create_task_group() as tg:
            for i in range(5):
                tg.start_soon(send_progress, i)

        # All should return the progress token
        assert all(result == "test-progress-token" for result in results)
        assert call_count == 5
        assert mock_time_limiter.reset.call_count == 5

    async def test_report_progress_logging(self, responder_no_token: Responder, caplog: pytest.LogCaptureFixture):
        """Test that warning is logged when progress token is not available."""
        with caplog.at_level("WARNING"):
            result = await responder_no_token.report_progress(50.0)

        assert result is None
        assert "report_progress failed: Progress token is not available" in caplog.text

    async def test_responder_attributes(self, responder: Responder):
        """Test that Responder has expected attributes."""
        assert hasattr(responder, "_request")
        assert hasattr(responder, "_progress_token")
        assert hasattr(responder, "_time_limiter")
        assert hasattr(responder, "_send")

    async def test_notification_json_structure(self, responder: Responder, mock_send: AsyncMock):
        """Test the exact JSON structure of notifications."""
        await responder.report_progress(42.5, 100.0, "Test progress")

        call_args = mock_send.call_args[0][0]
        notification_dict = json.loads(call_args)

        # Verify exact structure
        expected_structure = {
            "jsonrpc": "2.0",
            "method": "notifications/progress",
            "params": {
                "progressToken": "test-progress-token",
                "progress": 42.5,
                "total": 100.0,
                "message": "Test progress",
            },
        }

        assert notification_dict == expected_structure

    async def test_get_progress_token_with_different_request_types(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test progress token extraction with different request types."""
        # Since progress token extraction is complex and depends on MCP validation,
        # we'll test the behavior when manually setting the token
        responder = Responder("dummy_request", mock_send, mock_time_limiter)
        responder._progress_token = "prompt-progress-token"

        assert responder._progress_token == "prompt-progress-token"

    async def test_error_handling_in_get_progress_token(self, mock_send: AsyncMock, mock_time_limiter: Mock):
        """Test error handling in progress token extraction."""
        # Create a request that will cause an exception during token extraction
        malformed_request = '{"jsonrpc": "2.0", "id": 1, "method": "test"}'

        responder = Responder(malformed_request, mock_send, mock_time_limiter)

        assert responder._get_progress_token() is None


class TestResponderIntegration:
    """Integration tests for Responder class."""

    async def test_full_progress_reporting_workflow(self):
        """Test complete progress reporting workflow."""
        # Create a real TimeLimiter
        time_limiter = TimeLimiter(30.0)

        # Track sent messages
        sent_messages: list[str] = []

        async def track_send(message: str):
            sent_messages.append(message)

        # Create responder with mock token
        responder = Responder("dummy_request", track_send, time_limiter)
        responder._progress_token = "workflow-token"

        # Simulate a workflow with multiple progress updates
        progress_steps = [
            (10.0, 100.0, "Starting..."),
            (25.0, 100.0, "Processing input..."),
            (50.0, 100.0, "Halfway done..."),
            (75.0, 100.0, "Almost finished..."),
            (100.0, 100.0, "Complete!"),
        ]

        for progress, total, message in progress_steps:
            result = await responder.report_progress(progress, total, message)
            assert result == "workflow-token"

        # Verify all messages were sent
        assert len(sent_messages) == len(progress_steps)

        # Verify message contents
        for i, (progress, total, message) in enumerate(progress_steps):
            notification = json.loads(sent_messages[i])
            assert notification["params"]["progress"] == progress
            assert notification["params"]["total"] == total
            assert notification["params"]["message"] == message

    async def test_responder_with_real_time_limiter(self):
        """Test Responder with a real TimeLimiter."""
        time_limiter = TimeLimiter(1.0)  # 1 second timeout

        sent_messages: list[str] = []

        async def track_send(message: str):
            sent_messages.append(message)

        # Create responder with mock token
        responder = Responder("dummy_request", track_send, time_limiter)
        responder._progress_token = "real-timer-token"

        # Report progress and verify timer is reset
        original_deadline = time_limiter._scope.deadline
        await anyio.sleep(0.01)  # Small delay to ensure time passes
        await responder.report_progress(50.0)
        new_deadline = time_limiter._scope.deadline

        # Deadline should be updated (reset) - may be equal due to timing
        assert new_deadline >= original_deadline
        assert len(sent_messages) == 1

    async def test_error_recovery(self):
        """Test error recovery in responder operations."""
        time_limiter = TimeLimiter(30.0)

        # Create a send function that fails sometimes
        call_count = 0

        async def unreliable_send(message: str):
            nonlocal call_count
            call_count += 1
            if call_count == 2:  # Fail on second call
                raise ConnectionError("Network error")
            # Succeed on other calls

        # Create responder with mock token
        responder = Responder("dummy_request", unreliable_send, time_limiter)
        responder._progress_token = "recovery-token"

        # First call should succeed
        result1 = await responder.report_progress(25.0)
        assert result1 == "recovery-token"

        # Second call should fail
        with pytest.raises(ConnectionError):
            await responder.report_progress(50.0)

        # Third call should succeed again
        result3 = await responder.report_progress(75.0)
        assert result3 == "recovery-token"

    async def test_responder_without_progress_functionality(self):
        """Test responder when progress functionality is not needed."""
        time_limiter = TimeLimiter(30.0)

        sent_messages: list[str] = []

        async def track_send(message: str):
            sent_messages.append(message)

        # Request without progress token
        request = json.dumps(
            {"jsonrpc": "2.0", "id": 1, "method": "simple_tool", "params": {"name": "simple_tool", "arguments": {}}}
        )

        responder = Responder(request, track_send, time_limiter)

        # Progress reporting should be no-op
        result = await responder.report_progress(100.0)
        assert result is None
        assert len(sent_messages) == 0

        # But direct notification sending should still work
        notification = types.ServerNotification(
            types.ProgressNotification(
                method="notifications/progress",
                params=types.ProgressNotificationParams(progressToken="manual-token", progress=100.0),
            )
        )

        await responder.send_notification(notification)
        assert len(sent_messages) == 1
