"""Tests for SQ/EQ integration into app.py and portal.py (Phase 4)

Test scenarios:
- app.py initializes event_queue and submission_queue on app.state
- portal WebSocket emits correct event types to EQ
- EQ emit failure does not break WebSocket flow
- Events contain correct session_id and task_id
- _emit_event_safe helper handles None EQ gracefully
"""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from agentkit.core.event_queue import EventQueue, SubmissionQueue
from agentkit.core.protocol import (
    Event,
    TaskEventType,
    TurnEventType,
)
from agentkit.llm.gateway import LLMGateway
from agentkit.llm.protocol import LLMResponse, TokenUsage
from agentkit.server.app import create_app
from agentkit.server.routes.portal import _emit_event_safe
from agentkit.skills.registry import SkillRegistry
from agentkit.tools.registry import ToolRegistry


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_llm_gateway():
    gateway = LLMGateway()
    mock_provider = AsyncMock()
    mock_provider.chat.return_value = LLMResponse(
        content="Hello from LLM",
        model="test-model",
        usage=TokenUsage(prompt_tokens=10, completion_tokens=20),
    )
    gateway.register_provider("test", mock_provider)
    return gateway


@pytest.fixture
def skill_registry():
    return SkillRegistry()


@pytest.fixture
def tool_registry():
    return ToolRegistry()


@pytest.fixture
def app(mock_llm_gateway, skill_registry, tool_registry):
    return create_app(
        llm_gateway=mock_llm_gateway,
        skill_registry=skill_registry,
        tool_registry=tool_registry,
    )


# ---------------------------------------------------------------------------
# app.py initialization tests
# ---------------------------------------------------------------------------


class TestAppInitialization:
    """Verify app.py initializes SQ/EQ on app.state."""

    def test_app_has_event_queue(self, app):
        """app.state should have an EventQueue instance."""
        assert hasattr(app.state, "event_queue")
        assert isinstance(app.state.event_queue, EventQueue)

    def test_app_has_submission_queue(self, app):
        """app.state should have a SubmissionQueue instance."""
        assert hasattr(app.state, "submission_queue")
        assert isinstance(app.state.submission_queue, SubmissionQueue)

    def test_event_queue_initially_open(self, app):
        """EventQueue should be open (not closed) after app creation."""
        assert app.state.event_queue.is_closed is False

    def test_submission_queue_initially_open(self, app):
        """SubmissionQueue should be open (not closed) after app creation."""
        assert app.state.submission_queue.is_closed is False

    def test_event_queue_supports_subscribers(self, app):
        """EventQueue should start with zero subscribers."""
        assert app.state.event_queue.subscriber_count == 0


# ---------------------------------------------------------------------------
# _emit_event_safe helper tests
# ---------------------------------------------------------------------------


class TestEmitEventSafe:
    """Verify the _emit_event_safe helper handles edge cases."""

    @pytest.mark.asyncio
    async def test_emit_with_none_queue_is_noop(self):
        """Emitting to None queue should be a no-op (no exception)."""
        # No exception should be raised
        await _emit_event_safe(
            None,
            TaskEventType.TASK_CREATED,
            task_id="test-task",
            session_id="test-session",
            data={"message": "hello"},
        )

    @pytest.mark.asyncio
    async def test_emit_with_valid_queue_emits_event(self):
        """Emitting to a valid EventQueue should emit the event."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id="test-task",
            session_id="test-session",
            data={"message": "hello"},
        )
        # Buffer should contain the event
        assert len(eq._buffer) == 1
        event = eq._buffer[0]
        assert event.event_type == TaskEventType.TASK_CREATED
        assert event.task_id == "test-task"
        assert event.session_id == "test-session"
        assert event.data == {"message": "hello"}

    @pytest.mark.asyncio
    async def test_emit_with_failing_queue_does_not_raise(self):
        """Emit failures should be swallowed (never raise to caller)."""

        # Create a broken EventQueue that raises on emit
        class BrokenEventQueue(EventQueue):
            async def emit(self, event: Event) -> None:
                raise RuntimeError("Simulated EQ failure")

        broken_eq = BrokenEventQueue()
        # Should NOT raise — error is caught and logged
        await _emit_event_safe(
            broken_eq,
            TaskEventType.TASK_CREATED,
            task_id="test-task",
            session_id="test-session",
        )

    @pytest.mark.asyncio
    async def test_emit_with_none_data_uses_empty_dict(self):
        """Emitting with data=None should default to empty dict."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_STARTED,
            task_id="t1",
            session_id="s1",
            data=None,
        )
        assert eq._buffer[0].data == {}


# ---------------------------------------------------------------------------
# Portal WebSocket EQ integration tests
# ---------------------------------------------------------------------------


class TestPortalWebSocketEQIntegration:
    """Verify portal WebSocket emits events to EQ alongside WebSocket messages.

    NOTE: Starlette TestClient's sync WS client has known issues with disconnect
    handling (see test_portal_routes.py::TestPortalWebSocket for skipped tests).
    These tests verify EQ integration through the helper function and app state
    rather than full end-to-end WS flows.
    """

    @pytest.mark.asyncio
    async def test_app_state_event_queue_is_functional(self, app):
        """The EventQueue on app.state should be functional (accept emits)."""
        eq: EventQueue = app.state.event_queue

        # Emit a task.created event as the WebSocket handler would
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id="test-task-id",
            session_id="test-session-id",
            data={"message": "hello"},
        )

        # Verify the event was buffered
        assert len(eq._buffer) == 1
        event = eq._buffer[0]
        assert event.event_type == "task.created"
        assert event.task_id == "test-task-id"
        assert event.session_id == "test-session-id"

    @pytest.mark.asyncio
    async def test_eq_emit_failure_does_not_break_app(self, app):
        """If EQ.emit() raises, the app should continue to function normally."""

        # Replace EQ with a broken one that raises on emit
        class BrokenEventQueue(EventQueue):
            async def emit(self, event: Event) -> None:
                raise RuntimeError("Simulated EQ failure")

        app.state.event_queue = BrokenEventQueue()

        # The _emit_event_safe helper should swallow the error
        await _emit_event_safe(
            app.state.event_queue,
            TaskEventType.TASK_CREATED,
            task_id="t1",
            session_id="s1",
        )

        # App state should still be accessible
        assert app.state.event_queue is not None

    @pytest.mark.asyncio
    async def test_event_queue_close_on_shutdown(self):
        """EventQueue should be closed when app shuts down."""
        app = create_app(
            llm_gateway=LLMGateway(),
            skill_registry=SkillRegistry(),
            tool_registry=ToolRegistry(),
        )
        eq = app.state.event_queue
        sq = app.state.submission_queue

        assert eq.is_closed is False
        assert sq.is_closed is False

        # Simulate lifespan shutdown by calling close directly
        # (TestClient lifespan handling would also work but is heavier)
        eq.close()
        sq.close()

        assert eq.is_closed is True
        assert sq.is_closed is True

    @pytest.mark.asyncio
    async def test_full_task_lifecycle_events_simulated(self, app):
        """Simulate the full task lifecycle that the WebSocket handler would emit.

        This verifies that the EventQueue correctly receives and buffers
        the sequence of events emitted during a typical WebSocket chat flow.
        """
        eq: EventQueue = app.state.event_queue
        task_id = "simulated-task-123"
        session_id = "simulated-session-456"

        # Simulate the event sequence from portal_websocket handler
        await _emit_event_safe(
            eq, TaskEventType.TASK_CREATED, task_id, session_id, {"message": "hello"}
        )
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_STARTED,
            task_id,
            session_id,
            {"agent_name": "default", "execution_mode": "direct_chat"},
        )
        await _emit_event_safe(
            eq,
            TurnEventType.FINAL_ANSWER,
            task_id,
            session_id,
            {"output": "Hello back!"},
        )
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_COMPLETED,
            task_id,
            session_id,
            {"output": "Hello back!"},
        )

        # Verify all 4 events were buffered in order
        assert len(eq._buffer) == 4
        event_types = [e.event_type for e in eq._buffer]
        assert event_types == [
            "task.created",
            "task.started",
            "turn.final_answer",
            "task.completed",
        ]

        # Verify all events carry the same task_id and session_id
        for event in eq._buffer:
            assert event.task_id == task_id
            assert event.session_id == session_id


# ---------------------------------------------------------------------------
# Event type correctness tests
# ---------------------------------------------------------------------------


class TestEventTypes:
    """Verify event type constants are correctly used."""

    @pytest.mark.asyncio
    async def test_task_created_event_type(self):
        """task.created should map to TaskEventType.TASK_CREATED."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id="t1",
            session_id="s1",
            data={"message": "test"},
        )
        event = eq._buffer[0]
        assert event.event_type == "task.created"

    @pytest.mark.asyncio
    async def test_task_started_event_type(self):
        """task.started should map to TaskEventType.TASK_STARTED."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_STARTED,
            task_id="t1",
            session_id="s1",
        )
        event = eq._buffer[0]
        assert event.event_type == "task.started"

    @pytest.mark.asyncio
    async def test_task_completed_event_type(self):
        """task.completed should map to TaskEventType.TASK_COMPLETED."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_COMPLETED,
            task_id="t1",
            session_id="s1",
            data={"output": "done"},
        )
        event = eq._buffer[0]
        assert event.event_type == "task.completed"

    @pytest.mark.asyncio
    async def test_task_failed_event_type(self):
        """task.failed should map to TaskEventType.TASK_FAILED."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_FAILED,
            task_id="t1",
            session_id="s1",
            data={"error": "oops"},
        )
        event = eq._buffer[0]
        assert event.event_type == "task.failed"

    @pytest.mark.asyncio
    async def test_turn_thinking_event_type(self):
        """turn.thinking should map to TurnEventType.THINKING."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TurnEventType.THINKING,
            task_id="t1",
            session_id="s1",
        )
        event = eq._buffer[0]
        assert event.event_type == "turn.thinking"

    @pytest.mark.asyncio
    async def test_turn_tool_call_event_type(self):
        """turn.tool_call should map to TurnEventType.TOOL_CALL."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TurnEventType.TOOL_CALL,
            task_id="t1",
            session_id="s1",
        )
        event = eq._buffer[0]
        assert event.event_type == "turn.tool_call"

    @pytest.mark.asyncio
    async def test_turn_tool_result_event_type(self):
        """turn.tool_result should map to TurnEventType.TOOL_RESULT."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TurnEventType.TOOL_RESULT,
            task_id="t1",
            session_id="s1",
        )
        event = eq._buffer[0]
        assert event.event_type == "turn.tool_result"

    @pytest.mark.asyncio
    async def test_turn_final_answer_event_type(self):
        """turn.final_answer should map to TurnEventType.FINAL_ANSWER."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TurnEventType.FINAL_ANSWER,
            task_id="t1",
            session_id="s1",
        )
        event = eq._buffer[0]
        assert event.event_type == "turn.final_answer"


# ---------------------------------------------------------------------------
# Event field correctness tests
# ---------------------------------------------------------------------------


class TestEventFields:
    """Verify events contain correct session_id and task_id."""

    @pytest.mark.asyncio
    async def test_event_carries_correct_task_id(self):
        """Emitted event should carry the provided task_id."""
        eq = EventQueue()
        expected_task_id = "unique-task-id-123"
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id=expected_task_id,
            session_id="s1",
        )
        assert eq._buffer[0].task_id == expected_task_id

    @pytest.mark.asyncio
    async def test_event_carries_correct_session_id(self):
        """Emitted event should carry the provided session_id."""
        eq = EventQueue()
        expected_session_id = "conv-id-456"
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id="t1",
            session_id=expected_session_id,
        )
        assert eq._buffer[0].session_id == expected_session_id

    @pytest.mark.asyncio
    async def test_event_has_iso_timestamp(self):
        """Emitted event should have a non-empty ISO 8601 timestamp."""
        eq = EventQueue()
        await _emit_event_safe(
            eq,
            TaskEventType.TASK_CREATED,
            task_id="t1",
            session_id="s1",
        )
        timestamp = eq._buffer[0].timestamp
        assert isinstance(timestamp, str)
        assert len(timestamp) > 0
        # ISO 8601 format should contain 'T' separator
        assert "T" in timestamp

    @pytest.mark.asyncio
    async def test_multiple_events_preserve_order(self):
        """Multiple events should be buffered in emission order."""
        eq = EventQueue()
        await _emit_event_safe(eq, TaskEventType.TASK_CREATED, "t1", "s1")
        await _emit_event_safe(eq, TaskEventType.TASK_STARTED, "t1", "s1")
        await _emit_event_safe(eq, TurnEventType.FINAL_ANSWER, "t1", "s1")
        await _emit_event_safe(eq, TaskEventType.TASK_COMPLETED, "t1", "s1")

        event_types = [e.event_type for e in eq._buffer]
        assert event_types == [
            "task.created",
            "task.started",
            "turn.final_answer",
            "task.completed",
        ]


# ---------------------------------------------------------------------------
# SubmissionQueue integration tests
# ---------------------------------------------------------------------------


class TestSubmissionQueueIntegration:
    """Verify SubmissionQueue is properly initialized on app.state."""

    @pytest.mark.asyncio
    async def test_submission_queue_accepts_submissions(self, app):
        """SubmissionQueue on app.state should accept submissions."""
        sq: SubmissionQueue = app.state.submission_queue
        task_id = await sq.submit("hello", "session-1")
        assert isinstance(task_id, str)
        assert len(task_id) > 0

    @pytest.mark.asyncio
    async def test_submission_queue_close_marks_closed(self, app):
        """Closing SubmissionQueue should mark it as closed."""
        sq: SubmissionQueue = app.state.submission_queue
        assert sq.is_closed is False
        sq.close()
        assert sq.is_closed is True

    @pytest.mark.asyncio
    async def test_submission_queue_rejects_after_close(self, app):
        """Closed SubmissionQueue should reject new submissions."""
        sq: SubmissionQueue = app.state.submission_queue
        sq.close()
        with pytest.raises(RuntimeError, match="SubmissionQueue is closed"):
            await sq.submit("hello", "session-1")
