"""Tests for session-store: selector, registry, detector, store, models."""

from datetime import UTC, datetime

import pytest

from session_store import (
    AutoDetector,
    MessageNotFoundError,
    MessageSelector,
    NoActiveSessionError,
    SessionExport,
    SessionInfo,
    SessionMessage,
    SessionNotFoundError,
    SessionSource,
    SessionStore,
    SourceRegistry,
)
from session_store.registry import default_registry

# ─── Helpers ──────────────────────────────────────────────────────────────


def make_message(
    role: str = "assistant",
    content: str = "This is a sufficiently long assistant message for testing purposes.",
    node_id: str = "node-1",
    parent_node_id: str | None = None,
    created_at: datetime | None = None,
) -> SessionMessage:
    return SessionMessage(
        tool="test",
        session_id="sess-1",
        node_id=node_id,
        parent_node_id=parent_node_id,
        message_id=node_id,
        role=role,
        content=content,
        created_at=created_at or datetime(2026, 7, 14, 10, 0, 0, tzinfo=UTC),
    )


def make_session(messages: list[SessionMessage]) -> SessionExport:
    return SessionExport(
        tool="test",
        session_id="sess-1",
        title="Test Session",
        working_directory="/tmp/test",
        messages=messages,
    )


def make_session_info(
    tool: str = "test",
    session_id: str = "sess-1",
    title: str = "Test",
    message_count: int = 10,
    last_activity: datetime | None = None,
    working_directory: str = "/tmp/test",
) -> SessionInfo:
    return SessionInfo(
        tool=tool,
        session_id=session_id,
        title=title,
        message_count=message_count,
        last_activity=last_activity or datetime(2026, 7, 14, 10, 0, 0, tzinfo=UTC),
        working_directory=working_directory,
    )


class MockSource:
    """Mock SessionSource for testing."""

    def __init__(
        self,
        tool: str = "mock",
        sessions: list[SessionInfo] | None = None,
        exports: dict[str, SessionExport] | None = None,
        current_session: str | None = "mock-sess-1",
    ) -> None:
        self._tool = tool
        self._sessions = sessions or []
        self._exports = exports or {}
        self._current = current_session

    def tool_name(self) -> str:
        return self._tool

    def list_sessions(self, limit: int = 20) -> list[SessionInfo]:
        return self._sessions[:limit]

    def load_session(self, session_id: str) -> SessionExport:
        if session_id not in self._exports:
            raise SessionNotFoundError(f"Session '{session_id}' not found")
        return self._exports[session_id]

    def detect_current_session(self) -> str | None:
        return self._current


# ─── Model tests ──────────────────────────────────────────────────────────


class TestSessionExport:
    def test_assistant_texts_filters_by_role(self):
        msgs = [
            make_message(role="user", content="A" * 60, node_id="n1"),
            make_message(role="assistant", content="B" * 60, node_id="n2"),
            make_message(role="system", content="C" * 60, node_id="n3"),
        ]
        session = make_session(msgs)
        texts = session.assistant_texts
        assert len(texts) == 1
        assert texts[0].content == "B" * 60

    def test_assistant_texts_filters_short_content(self):
        msgs = [
            make_message(role="assistant", content="short", node_id="n1"),
            make_message(role="assistant", content="B" * 60, node_id="n2"),
        ]
        session = make_session(msgs)
        texts = session.assistant_texts
        assert len(texts) == 1
        assert texts[0].content == "B" * 60

    def test_assistant_texts_deduplicates(self):
        content = "D" * 60
        msgs = [
            make_message(role="assistant", content=content, node_id="n1"),
            make_message(role="assistant", content=content, node_id="n2"),
        ]
        session = make_session(msgs)
        texts = session.assistant_texts
        assert len(texts) == 1


# ─── MessageSelector tests ────────────────────────────────────────────────


class TestMessageSelector:
    def setup_method(self):
        self.selector = MessageSelector()
        self.messages = [
            make_message(role="user", content="user question", node_id="u1"),
            make_message(role="assistant", content="A" * 60, node_id="a1"),
            make_message(role="assistant", content="B" * 60, node_id="a2"),
            make_message(role="assistant", content="C" * 60, node_id="a3"),
        ]
        self.session = make_session(self.messages)

    def test_latest(self):
        msg = self.selector.resolve("latest", self.session)
        assert msg.content == "C" * 60

    def test_head_alias(self):
        msg = self.selector.resolve("HEAD", self.session)
        assert msg.content == "C" * 60

    def test_head_til_n(self):
        msg = self.selector.resolve("HEAD~1", self.session)
        assert msg.content == "B" * 60
        msg = self.selector.resolve("HEAD~2", self.session)
        assert msg.content == "A" * 60

    def test_head_til_out_of_range(self):
        with pytest.raises(MessageNotFoundError):
            self.selector.resolve("HEAD~10", self.session)

    def test_number(self):
        msg = self.selector.resolve("1", self.session)
        assert msg.content == "A" * 60
        msg = self.selector.resolve("3", self.session)
        assert msg.content == "C" * 60

    def test_number_out_of_range(self):
        with pytest.raises(MessageNotFoundError):
            self.selector.resolve("10", self.session)

    def test_first(self):
        msg = self.selector.resolve("first", self.session)
        assert msg.content == "A" * 60

    def test_search(self):
        msg = self.selector.resolve("search:B", self.session)
        assert msg.content == "B" * 60

    def test_search_case_insensitive(self):
        msg = self.selector.resolve("search:b", self.session)
        assert msg.content == "B" * 60

    def test_search_not_found(self):
        with pytest.raises(MessageNotFoundError):
            self.selector.resolve("search:nonexistent", self.session)

    def test_unknown_target(self):
        with pytest.raises(ValueError, match="Unknown target"):
            self.selector.resolve("invalid", self.session)

    def test_no_assistant_messages(self):
        msgs = [make_message(role="user", content="X" * 60, node_id="u1")]
        session = make_session(msgs)
        with pytest.raises(MessageNotFoundError):
            self.selector.resolve("latest", session)

    def test_filter_assistant(self):
        filtered = self.selector.filter_assistant(self.messages)
        assert len(filtered) == 3
        assert all(m.role == "assistant" for m in filtered)

    def test_filter_assistant_dedup(self):
        content = "D" * 60
        msgs = [
            make_message(role="assistant", content=content, node_id="n1"),
            make_message(role="assistant", content=content, node_id="n2"),
        ]
        filtered = self.selector.filter_assistant(msgs)
        assert len(filtered) == 1

    def test_filter_assistant_short_excluded(self):
        msgs = [
            make_message(role="assistant", content="short", node_id="n1"),
            make_message(role="assistant", content="L" * 60, node_id="n2"),
        ]
        filtered = self.selector.filter_assistant(msgs)
        assert len(filtered) == 1


# ─── SourceRegistry tests ─────────────────────────────────────────────────


class TestSourceRegistry:
    def test_register_and_get(self):
        registry = SourceRegistry()
        source = MockSource(tool="mock")
        registry.register(source)
        assert registry.get("mock") is source

    def test_get_not_found(self):
        registry = SourceRegistry()
        with pytest.raises(KeyError):
            registry.get("nonexistent")

    def test_all_sources(self):
        registry = SourceRegistry()
        s1 = MockSource(tool="a")
        s2 = MockSource(tool="b")
        registry.register(s1)
        registry.register(s2)
        assert len(registry.all_sources()) == 2

    def test_tool_names(self):
        registry = SourceRegistry()
        registry.register(MockSource(tool="devin"))
        registry.register(MockSource(tool="opencode"))
        names = registry.tool_names()
        assert "devin" in names
        assert "opencode" in names

    def test_default_registry(self):
        registry = default_registry()
        names = registry.tool_names()
        assert "devin" in names
        assert "opencode" in names
        assert "copilot" in names


# ─── AutoDetector tests ───────────────────────────────────────────────────


class TestAutoDetector:
    def test_detect_tool_most_recent(self):
        registry = SourceRegistry()
        registry.register(
            MockSource(
                tool="devin",
                sessions=[
                    make_session_info(
                        tool="devin", last_activity=datetime(2026, 7, 14, 10, 0, tzinfo=UTC)
                    )
                ],
            )
        )
        registry.register(
            MockSource(
                tool="opencode",
                sessions=[
                    make_session_info(
                        tool="opencode", last_activity=datetime(2026, 7, 14, 12, 0, tzinfo=UTC)
                    )
                ],
            )
        )
        detector = AutoDetector(registry)
        assert detector.detect_tool() == "opencode"

    def test_detect_tool_no_sessions(self):
        registry = SourceRegistry()
        registry.register(MockSource(tool="devin", sessions=[]))
        detector = AutoDetector(registry)
        with pytest.raises(NoActiveSessionError):
            detector.detect_tool()

    def test_detect_session_by_cwd(self):
        registry = SourceRegistry()
        registry.register(
            MockSource(
                tool="devin",
                sessions=[
                    make_session_info(
                        tool="devin",
                        session_id="sess-a",
                        working_directory="/other",
                        last_activity=datetime(2026, 7, 14, 10, 0, tzinfo=UTC),
                    ),
                    make_session_info(
                        tool="devin",
                        session_id="sess-b",
                        working_directory="/my/project",
                        last_activity=datetime(2026, 7, 14, 11, 0, tzinfo=UTC),
                    ),
                ],
            )
        )
        detector = AutoDetector(registry)
        result = detector.detect_session("devin", cwd="/my/project")
        assert result == "sess-b"

    def test_detect_session_fallback_to_source(self):
        registry = SourceRegistry()
        registry.register(
            MockSource(
                tool="devin",
                sessions=[
                    make_session_info(tool="devin", session_id="sess-a", working_directory="/other")
                ],
                current_session="detected-sess",
            )
        )
        detector = AutoDetector(registry)
        result = detector.detect_session("devin", cwd="/nonexistent")
        assert result == "detected-sess"

    def test_detect_session_no_cwd(self):
        registry = SourceRegistry()
        registry.register(
            MockSource(
                tool="devin",
                current_session="fallback-sess",
            )
        )
        detector = AutoDetector(registry)
        result = detector.detect_session("devin")
        assert result == "fallback-sess"


# ─── SessionStore tests ───────────────────────────────────────────────────


class TestSessionStore:
    def _make_store(self) -> tuple[SessionStore, SourceRegistry]:
        registry = SourceRegistry()
        msgs = [
            make_message(role="assistant", content="A" * 60, node_id="a1"),
            make_message(role="assistant", content="B" * 60, node_id="a2"),
        ]
        export = make_session(msgs)
        registry.register(
            MockSource(
                tool="devin",
                sessions=[make_session_info(tool="devin", session_id="sess-1")],
                exports={"sess-1": export},
                current_session="sess-1",
            )
        )
        store = SessionStore(registry=registry)
        return store, registry

    def test_list_sessions_all_tools(self):
        store, _ = self._make_store()
        sessions = store.list_sessions()
        assert len(sessions) == 1
        assert sessions[0].tool == "devin"

    def test_list_sessions_specific_tool(self):
        store, _ = self._make_store()
        sessions = store.list_sessions(tool="devin")
        assert len(sessions) == 1
        assert sessions[0].session_id == "sess-1"

    def test_get_session(self):
        store, _ = self._make_store()
        session = store.get_session("sess-1", tool="devin")
        assert session.session_id == "sess-1"
        assert len(session.messages) == 2

    def test_get_session_not_found(self):
        store, _ = self._make_store()
        with pytest.raises(SessionNotFoundError):
            store.get_session("nonexistent", tool="devin")

    def test_get_session_searches_all_sources(self):
        store, _ = self._make_store()
        session = store.get_session("sess-1")
        assert session.session_id == "sess-1"

    def test_get_messages(self):
        store, _ = self._make_store()
        messages = store.get_messages("sess-1", tool="devin")
        assert len(messages) == 2

    def test_select_message_with_session_id(self):
        store, _ = self._make_store()
        msg = store.select_message("latest", session_id="sess-1", tool="devin")
        assert msg.content == "B" * 60

    def test_select_message_auto_detect(self):
        store, _ = self._make_store()
        msg = store.select_message("latest")
        assert msg.content == "B" * 60

    def test_detect_tool(self):
        store, _ = self._make_store()
        assert store.detect_tool() == "devin"

    def test_detect_session(self):
        store, _ = self._make_store()
        assert store.detect_session(tool="devin") == "sess-1"


# ─── Protocol test ────────────────────────────────────────────────────────


class TestSessionSourceProtocol:
    def test_mock_source_is_runtime_checkable(self):
        source = MockSource()
        assert isinstance(source, SessionSource)
