"""MCP server test doubles for unified testing.

This module provides the MCPServerProtocol and FakeMCPServer implementations
that can be imported by test modules without pytest dependency issues.

Spec source: vectl step integration-test-seams.unify-fake-mcp.
"""

from __future__ import annotations

import asyncio
from typing import Protocol, runtime_checkable

from returns.result import Success


@runtime_checkable
class MCPServerProtocol(Protocol):
    """Protocol defining the MCP server interface for agent communication.

    This protocol defines the minimal interface that MCP server implementations
    must provide for testing AgentRunner interactions. It matches the real
    AgentMCPServer interface from src/anima/shell/mcp_agent.py.

    Used by tests to create minimal fake implementations without full MCP setup.

    Spec source: vectl step integration-test-seams.unify-fake-mcp.

    Attributes:
        instance_id: Unique identifier for the agent instance.

    Methods:
        start(): Start the MCP server (async).
        stop(): Stop the MCP server (async).
        get_pending_message(): Get message queue for agent loop integration.
        drain_pending_messages(): Drain all queued messages.
        set_response(): Set response for last interact call.
        set_busy(): Mark agent as busy/idle during interact processing.
        get_interrupt_event(): Get event for graceful shutdown signaling.
        get_stop_turn_event(): Get event for canceling current interact turn.
        start_stream_session(): Initialize stream state for a new interact turn.
        is_stream_active(): Check if stream is currently active.
    """

    instance_id: str

    async def start(self) -> Success[None]:
        """Start the MCP server.

        Returns:
            Success(None) on successful start.
        """
        ...

    async def stop(self) -> None:
        """Stop the MCP server and clean up resources."""
        ...

    def get_pending_message(self) -> asyncio.Queue[str]:
        """Return the pending message queue for agent loop integration.

        Returns:
            The message queue (always initialized).
        """
        ...

    def drain_pending_messages(self) -> list[str]:
        """Drain and return currently queued interact messages.

        Returns:
            Messages available at drain time, oldest first.
        """
        ...

    def set_response(self, response: str, turn_number: int) -> None:
        """Set the response for the last interact call.

        Args:
            response: The agent's response text.
            turn_number: The turn number after processing.
        """
        ...

    def set_busy(self, is_busy: bool) -> None:
        """Set whether agent is actively processing an interact turn.

        Args:
            is_busy: True while processing interact turn, else False.
        """
        ...

    def get_interrupt_event(self) -> asyncio.Event:
        """Return or create the interrupt event for agent loop integration.

        Returns:
            The interrupt event that can be awaited to detect interrupt requests.
        """
        ...

    def get_stop_turn_event(self) -> asyncio.Event:
        """Return or create the stop-turn event for control channel integration.

        Returns:
            The stop-turn event for canceling current interact turn.
        """
        ...

    async def start_stream_session(self, turn_number: int) -> None:
        """Initialize stream state for a new interact turn.

        Args:
            turn_number: The turn number for this stream session.
        """
        ...

    async def enqueue_stream_chunk(self, turn_number: int, chunk: str) -> None:
        """Enqueue a text chunk for streaming.

        Args:
            turn_number: Current turn number for event correlation.
            chunk: Text delta from accumulated assistant output.
        """
        ...

    async def enqueue_stream_done(
        self,
        turn_number: int,
        stop_reason: str,
        final_response_chars: int,
        prompt_tokens: int,
        completion_tokens: int,
        total_tokens: int,
    ) -> None:
        """Enqueue done event with usage snapshot.

        Args:
            turn_number: Current turn number for event correlation.
            stop_reason: "complete" or "stopped" per design doc.
            final_response_chars: Character count of final response text.
            prompt_tokens: Prompt token count.
            completion_tokens: Completion token count.
            total_tokens: Total token count.
        """
        ...

    async def enqueue_stream_error(
        self,
        turn_number: int,
        error: str,
        code: str,
    ) -> None:
        """Enqueue error event.

        Args:
            turn_number: Current turn number for event correlation.
            error: Human-readable error message.
            code: Error code.
        """
        ...

    def cancel_stream(self) -> None:
        """Signal stream cancellation (client disconnected)."""
        ...

    def is_stream_active(self) -> bool:
        """Check if stream is currently active.

        Returns:
            True if stream session is active, False otherwise.
        """
        ...


class FakeMCPServer:
    """Minimal MCP server implementation for testing AgentRunner.

    This is a simplified test double that implements MCPServerProtocol
    without full FastMCP setup. Used by tests that need to inject fake
    MCP servers into AgentRunner.

    Spec source: vectl step integration-test-seams.unify-fake-mcp.

    Attributes:
        instance_id: Placeholder ID (tests typically don't need this).
        _pending_queue: Queue for pending messages.
        _response: Last response tuple (response_text, turn_number).
        _busy: Whether agent is processing an interact turn.
        _interrupt_event: Event for interrupt signaling.
        _stop_turn_event: Event for stop-turn signaling.
    """

    def __init__(
        self,
        instance_id: str = "inst_test",
        agent_runner: object | None = None,
        interact_timeout_seconds: float = 30.0,
    ) -> None:
        """Initialize the fake MCP server.

        Args:
            instance_id: Agent instance ID (optional, defaults to "inst_test").
            agent_runner: AgentRunner reference (optional, for compatibility with AgentMCPServer signature).
            interact_timeout_seconds: Timeout for interactions (optional, default 30.0).

        Note:
            The agent_runner and interact_timeout_seconds parameters are accepted for
            drop-in compatibility with AgentMCPServer's constructor signature but are
            not used by the fake implementation.
        """
        self.instance_id = instance_id
        self._agent_runner = agent_runner  # Stored for test introspection if needed
        self._interact_timeout_seconds = interact_timeout_seconds
        self._pending_queue: asyncio.Queue[str] = asyncio.Queue()
        self._response: tuple[str, int] | None = None
        self._busy: bool = False
        self._interrupt_event: asyncio.Event | None = None
        self._stop_turn_event: asyncio.Event | None = None
        self._response_event: asyncio.Event | None = None
        self.socket_path: str | None = None
        # Stream state for testing (Spec: talk-streaming-transport.md)
        self._stream_active: bool = False

    # Public attributes for test convenience
    @property
    def response(self) -> tuple[str, int] | None:
        """Get the last response tuple (response_text, turn_number)."""
        return self._response

    @property
    def busy(self) -> bool:
        """Get busy status."""
        return self._busy

    @property
    def stop_turn_event(self) -> asyncio.Event:
        """Alias for get_stop_turn_event() for test convenience."""
        return self.get_stop_turn_event()

    @property
    def queue(self) -> asyncio.Queue[str]:
        """Alias for get_pending_message() for test convenience."""
        return self._pending_queue

    async def start(self) -> Success[None]:
        """Start the MCP server.

        Returns:
            Success(None) - always succeeds in test doubles.
        """
        return Success(None)

    async def stop(self) -> None:
        """Stop the MCP server and clean up resources."""
        # No-op for test double
        pass

    def get_pending_message(self) -> asyncio.Queue[str]:
        """Return the pending message queue for agent loop integration.

        Returns:
            The message queue (always initialized in fake implementation).
        """
        return self._pending_queue

    def drain_pending_messages(self) -> list[str]:
        """Drain and return currently queued interact messages.

        Returns:
            Messages available at drain time, oldest first.
        """
        drained: list[str] = []
        while True:
            try:
                drained.append(self._pending_queue.get_nowait())
            except asyncio.QueueEmpty:
                break
        return drained

    def set_response(self, response: str, turn_number: int) -> None:
        """Set the response for the last interact call.

        Args:
            response: The agent's response text.
            turn_number: The turn number after processing.
        """
        self._response = (response, turn_number)
        if self._response_event is not None:
            self._response_event.set()

    def set_busy(self, is_busy: bool) -> None:
        """Set whether agent is actively processing an interact turn.

        Args:
            is_busy: True while processing interact turn, else False.
        """
        self._busy = is_busy

    def get_interrupt_event(self) -> asyncio.Event:
        """Return or create the interrupt event for agent loop integration.

        Returns:
            The interrupt event that can be awaited to detect interrupt requests.
        """
        if self._interrupt_event is None:
            self._interrupt_event = asyncio.Event()
        return self._interrupt_event

    def get_stop_turn_event(self) -> asyncio.Event:
        """Return or create the stop-turn event for control channel integration.

        Returns:
            The stop-turn event for canceling current interact turn.
        """
        if self._stop_turn_event is None:
            self._stop_turn_event = asyncio.Event()
        return self._stop_turn_event

    # Stream management methods (Spec: talk-streaming-transport.md)

    async def start_stream_session(self, turn_number: int) -> None:
        """Initialize stream state for a new interact turn.

        Args:
            turn_number: The turn number for this stream session.
        """
        self._stream_active = True

    async def enqueue_stream_chunk(self, turn_number: int, chunk: str) -> None:
        """Enqueue a text chunk for streaming (no-op in test double).

        Args:
            turn_number: Current turn number for event correlation.
            chunk: Text delta from accumulated assistant output.
        """
        # No-op in test double - actual streaming tested in integration tests
        pass

    async def enqueue_stream_done(
        self,
        turn_number: int,
        stop_reason: str,
        final_response_chars: int,
        prompt_tokens: int,
        completion_tokens: int,
        total_tokens: int,
    ) -> None:
        """Enqueue done event (no-op in test double).

        Args:
            turn_number: Current turn number for event correlation.
            stop_reason: "complete" or "stopped" per design doc.
            final_response_chars: Character count of final response text.
            prompt_tokens: Prompt token count.
            completion_tokens: Completion token count.
            total_tokens: Total token count.
        """
        # No-op in test double - actual streaming tested in integration tests
        pass

    async def enqueue_stream_error(
        self,
        turn_number: int,
        error: str,
        code: str = "INTERNAL",
    ) -> None:
        """Enqueue error event (no-op in test double).

        Args:
            turn_number: Current turn number for event correlation.
            error: Human-readable error message.
            code: Error code (default "INTERNAL").
        """
        # No-op in test double - actual streaming tested in integration tests
        pass

    def cancel_stream(self) -> None:
        """Signal stream cancellation (client disconnected)."""
        self._stream_active = False

    def is_stream_active(self) -> bool:
        """Check if stream is currently active.

        Returns:
            True if stream session is active, False otherwise.
        """
        return self._stream_active
