"""Tests for the thread state machine."""

from __future__ import annotations

import pytest
from icarus.session.thread_state import ThreadState, ThreadStateManager


class TestThreadState:
    """Verify enum values match the spec."""

    def test_all_states_defined(self) -> None:
        assert set(ThreadState) == {
            ThreadState.IDLE, ThreadState.QUEUED, ThreadState.STREAMING,
            ThreadState.HITL, ThreadState.DONE, ThreadState.ERROR,
        }

    def test_string_values(self) -> None:
        assert ThreadState.IDLE.value == "idle"
        assert ThreadState.STREAMING.value == "streaming"
        assert ThreadState.HITL.value == "hitl"


class TestThreadStateManager:
    """Verify state transitions, observers, and edge cases."""

    def test_unknown_thread_returns_idle(self) -> None:
        mgr = ThreadStateManager()
        assert mgr.get_state("unknown") == ThreadState.IDLE

    def test_valid_transition_idle_to_queued(self) -> None:
        mgr = ThreadStateManager()
        old = mgr.set_state("t1", ThreadState.QUEUED)
        assert old == ThreadState.IDLE
        assert mgr.get_state("t1") == ThreadState.QUEUED

    def test_valid_transition_idle_to_streaming(self) -> None:
        mgr = ThreadStateManager()
        old = mgr.set_state("t1", ThreadState.STREAMING)
        assert old == ThreadState.IDLE
        assert mgr.get_state("t1") == ThreadState.STREAMING

    def test_valid_transition_streaming_to_done(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        old = mgr.set_state("t1", ThreadState.DONE)
        assert old == ThreadState.STREAMING

    def test_valid_transition_streaming_to_hitl(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t1", ThreadState.HITL)
        assert mgr.get_state("t1") == ThreadState.HITL

    def test_valid_transition_hitl_to_streaming(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t1", ThreadState.HITL)
        mgr.set_state("t1", ThreadState.STREAMING)
        assert mgr.get_state("t1") == ThreadState.STREAMING

    def test_valid_transition_streaming_to_error(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        old = mgr.set_state("t1", ThreadState.ERROR)
        assert old == ThreadState.STREAMING
        assert mgr.get_state("t1") == ThreadState.ERROR

    def test_invalid_transition_raises(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t1", ThreadState.DONE)
        with pytest.raises(ValueError, match="Invalid transition"):
            mgr.set_state("t1", ThreadState.HITL)  # done -> hitl is invalid

    def test_idle_to_idle_noop_returns_none(self) -> None:
        mgr = ThreadStateManager()
        result = mgr.set_state("t1", ThreadState.IDLE)
        assert result is None

    def test_force_idle_from_any_state(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t1", ThreadState.HITL)
        mgr.force_idle("t1")
        assert mgr.get_state("t1") == ThreadState.IDLE

    def test_force_idle_unknown_thread_no_error(self) -> None:
        mgr = ThreadStateManager()
        mgr.force_idle("unknown")

    def test_get_all_active(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t2", ThreadState.STREAMING)
        mgr.set_state("t2", ThreadState.DONE)
        mgr.set_state("t3", ThreadState.QUEUED)
        active = mgr.get_all_active()
        assert set(active.keys()) == {"t1", "t2", "t3"}

    def test_get_all_active_excludes_idle(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.set_state("t1", ThreadState.DONE)
        mgr.force_idle("t1")
        assert mgr.get_all_active() == {}

    def test_clear_removes_thread(self) -> None:
        mgr = ThreadStateManager()
        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.clear("t1")
        assert mgr.get_state("t1") == ThreadState.IDLE
        assert "t1" not in mgr.get_all_active()


class TestThreadStateObserver:
    """Verify the observer/listener pattern."""

    def test_listener_called_on_transition(self) -> None:
        mgr = ThreadStateManager()
        calls: list[tuple[str, ThreadState, ThreadState]] = []
        mgr.on_change(lambda tid, old, new: calls.append((tid, old, new)))

        mgr.set_state("t1", ThreadState.STREAMING)
        assert calls == [("t1", ThreadState.IDLE, ThreadState.STREAMING)]

    def test_listener_called_on_force_idle(self) -> None:
        mgr = ThreadStateManager()
        calls: list[tuple] = []
        mgr.on_change(lambda tid, old, new: calls.append((tid, old, new)))

        mgr.set_state("t1", ThreadState.STREAMING)
        mgr.force_idle("t1")
        assert calls[-1] == ("t1", ThreadState.STREAMING, ThreadState.IDLE)

    def test_listener_not_called_on_noop(self) -> None:
        mgr = ThreadStateManager()
        calls: list[tuple] = []
        mgr.on_change(lambda tid, old, new: calls.append((tid, old, new)))

        mgr.set_state("t1", ThreadState.IDLE)
        assert calls == []

    def test_unsubscribe(self) -> None:
        mgr = ThreadStateManager()
        calls: list[tuple] = []
        unsub = mgr.on_change(lambda tid, old, new: calls.append((tid, old, new)))

        mgr.set_state("t1", ThreadState.STREAMING)
        assert len(calls) == 1

        unsub()
        mgr.set_state("t1", ThreadState.DONE)
        assert len(calls) == 1


class TestListThreadsStatus:
    """Verify list_threads includes status from state manager."""

    @pytest.mark.asyncio
    async def test_list_threads_without_state_manager(self) -> None:
        """Existing behavior -- no status field when no manager provided."""
        from unittest.mock import AsyncMock, MagicMock, patch

        from icarus.session.threads import list_threads

        mock_conn = AsyncMock()
        mock_cursor = AsyncMock()
        mock_cursor.fetchall = AsyncMock(return_value=[
            ("t1", "icarus", "2026-03-16T12:00:00"),
        ])
        mock_conn.execute = MagicMock(return_value=mock_cursor)
        mock_cursor.__aenter__ = AsyncMock(return_value=mock_cursor)
        mock_cursor.__aexit__ = AsyncMock(return_value=False)

        with patch("icarus.session.threads._connect") as mock_connect:
            mock_cm = AsyncMock()
            mock_cm.__aenter__ = AsyncMock(return_value=mock_conn)
            mock_cm.__aexit__ = AsyncMock(return_value=False)
            mock_connect.return_value = mock_cm

            with patch("icarus.session.threads._table_exists", return_value=True):
                threads = await list_threads()

        for t in threads:
            assert "status" not in t

    @pytest.mark.asyncio
    async def test_list_threads_returns_no_status(self) -> None:
        """Core list_threads does not include status — sidecar stamps it."""
        from unittest.mock import AsyncMock, MagicMock, patch

        from icarus.session.threads import list_threads

        mock_conn = AsyncMock()
        mock_cursor = AsyncMock()
        mock_cursor.fetchall = AsyncMock(return_value=[
            ("t1", "icarus", "2026-03-16T12:00:00"),
        ])
        mock_conn.execute = MagicMock(return_value=mock_cursor)
        mock_cursor.__aenter__ = AsyncMock(return_value=mock_cursor)
        mock_cursor.__aexit__ = AsyncMock(return_value=False)

        with patch("icarus.session.threads._connect") as mock_connect:
            mock_cm = AsyncMock()
            mock_cm.__aenter__ = AsyncMock(return_value=mock_conn)
            mock_cm.__aexit__ = AsyncMock(return_value=False)
            mock_connect.return_value = mock_cm

            with patch("icarus.session.threads._table_exists", return_value=True):
                threads = await list_threads()

        assert "status" not in threads[0]
