"""Unit tests for the session module."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest

from core.database import Database
from core.session import ContextManager, Session, SessionStore

# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------


class TestSession:
    """Tests for Session dataclass."""

    def test_new_creates_session_with_system_message(self) -> None:
        session = Session.new("You are helpful.")
        assert session.id
        assert len(session.id) == 12
        assert session.title == ""
        assert len(session.messages) == 1
        assert session.messages[0]["role"] == "system"
        assert session.messages[0]["content"] == "You are helpful."

    def test_new_with_title(self) -> None:
        session = Session.new("System prompt", title="My Session")
        assert session.title == "My Session"

    def test_touch_updates_timestamp(self) -> None:
        session = Session.new("test")
        original_updated = session.updated_at
        session.touch()
        assert session.updated_at >= original_updated

    def test_auto_title_from_first_user_message(self) -> None:
        session = Session.new("system")
        session.messages.append({"role": "user", "content": "Hello, how are you today?"})
        session.auto_title()
        assert session.title == "Hello, how are you today?"

    def test_auto_title_truncates_long_messages(self) -> None:
        session = Session.new("system")
        long_msg = "A" * 100
        session.messages.append({"role": "user", "content": long_msg})
        session.auto_title()
        assert len(session.title) == 63  # 60 + "..."
        assert session.title.endswith("...")

    def test_auto_title_does_not_overwrite_existing(self) -> None:
        session = Session.new("system", title="Existing Title")
        session.messages.append({"role": "user", "content": "New message"})
        session.auto_title()
        assert session.title == "Existing Title"

    def test_to_dict_and_from_dict_roundtrip(self) -> None:
        session = Session.new("You are helpful.", title="Test")
        session.messages.append({"role": "user", "content": "Hello"})
        session.messages.append({"role": "assistant", "content": "Hi there!"})
        session.metadata = {"model": "gpt-4o"}

        data = session.to_dict()
        restored = Session.from_dict(data)

        assert restored.id == session.id
        assert restored.title == session.title
        assert restored.messages == session.messages
        assert restored.metadata == session.metadata


# ---------------------------------------------------------------------------
# SessionStore
# ---------------------------------------------------------------------------


class TestSessionStore:
    """Tests for SessionStore class."""

    @pytest.fixture
    def store(self, tmp_path: Path) -> SessionStore:
        """Create a session store backed by a temp database."""
        db = Database(tmp_path / "test.db")
        return SessionStore(db)

    def test_save_and_load(self, store: SessionStore) -> None:
        session = Session.new("system", title="Test Session")
        session.messages.append({"role": "user", "content": "Hello"})

        store.save(session)
        loaded = store.load(session.id)

        assert loaded.id == session.id
        assert loaded.title == session.title
        assert len(loaded.messages) == 2

    def test_load_nonexistent_raises(self, store: SessionStore) -> None:
        with pytest.raises(FileNotFoundError):
            store.load("nonexistent")

    def test_list_sessions_empty(self, store: SessionStore) -> None:
        sessions = store.list_sessions()
        assert sessions == []

    def test_list_sessions_returns_summaries(self, store: SessionStore) -> None:
        s1 = Session.new("system", title="First")
        s1.messages.append({"role": "user", "content": "Hello"})
        store.save(s1)

        s2 = Session.new("system", title="Second")
        s2.messages.append({"role": "user", "content": "Hi"})
        s2.messages.append({"role": "assistant", "content": "Hello!"})
        store.save(s2)

        sessions = store.list_sessions()
        assert len(sessions) == 2

        # Check summary structure
        for s in sessions:
            assert "id" in s
            assert "title" in s
            assert "updated_at" in s
            assert "message_count" in s

    def test_list_sessions_sorted_newest_first(self, store: SessionStore) -> None:
        s1 = Session.new("system", title="Older")
        store.save(s1)

        s2 = Session.new("system", title="Newer")
        store.save(s2)

        sessions = store.list_sessions()
        assert sessions[0]["title"] == "Newer"
        assert sessions[1]["title"] == "Older"

    def test_list_sessions_respects_limit(self, store: SessionStore) -> None:
        for i in range(5):
            s = Session.new("system", title=f"Session {i}")
            store.save(s)

        sessions = store.list_sessions(limit=3)
        assert len(sessions) == 3

    def test_delete_existing(self, store: SessionStore) -> None:
        session = Session.new("system")
        store.save(session)

        result = store.delete(session.id)
        assert result is True

        with pytest.raises(FileNotFoundError):
            store.load(session.id)

    def test_delete_nonexistent(self, store: SessionStore) -> None:
        result = store.delete("nonexistent")
        assert result is False


# ---------------------------------------------------------------------------
# ContextManager
# ---------------------------------------------------------------------------


class TestContextManager:
    """Tests for ContextManager class."""

    @pytest.fixture
    def manager(self) -> ContextManager:
        """Create a context manager with small limits for testing."""
        return ContextManager(max_tokens=1000, reserve_tokens=100)

    def test_count_tokens(self, manager: ContextManager) -> None:
        tokens = manager.count_tokens("Hello, world!")
        assert tokens > 0
        assert isinstance(tokens, int)

    def test_prepare_messages_empty(self, manager: ContextManager) -> None:
        result = manager.prepare_messages([])
        assert result == []

    def test_prepare_messages_preserves_system(self, manager: ContextManager) -> None:
        messages = [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi there!"},
        ]
        result = manager.prepare_messages(messages)
        assert result[0]["role"] == "system"
        assert result[0]["content"] == "You are helpful."

    def test_prepare_messages_small_fits(self, manager: ContextManager) -> None:
        messages = [
            {"role": "system", "content": "System"},
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi"},
        ]
        result = manager.prepare_messages(messages)
        assert len(result) == 3

    def test_prepare_messages_truncates_oldest(self) -> None:
        # Very small limit to force truncation
        manager = ContextManager(max_tokens=50, reserve_tokens=10)
        messages = [
            {"role": "system", "content": "Sys"},
            {"role": "user", "content": "First message that is quite long and wordy"},
            {
                "role": "assistant",
                "content": "First reply that is also long and verbose",
            },
            {"role": "user", "content": "Second user message here"},
            {"role": "assistant", "content": "Second reply here"},
        ]
        result = manager.prepare_messages(messages)

        # System should be preserved
        assert result[0]["role"] == "system"

        # Should have fewer messages due to truncation
        assert len(result) < len(messages)

        # Newest messages should be kept
        assert result[-1]["content"] == "Second reply here"

    def test_prepare_messages_keeps_tool_groups_together(self, manager: ContextManager) -> None:
        """Tool calls and their responses should not be split."""
        messages: list[dict[str, Any]] = [
            {"role": "system", "content": "Sys"},
            {"role": "user", "content": "Use a tool"},
            {
                "role": "assistant",
                "content": None,
                "tool_calls": [{"id": "1", "function": {"name": "test"}}],
            },
            {"role": "tool", "tool_call_id": "1", "content": "Result"},
            {"role": "assistant", "content": "Done"},
        ]
        result = manager.prepare_messages(messages)

        # If tool call is present, its response should be too
        has_tool_call = any(m.get("tool_calls") for m in result)
        has_tool_response = any(m.get("role") == "tool" for m in result)

        if has_tool_call:
            assert has_tool_response

    def test_group_into_turns(self, manager: ContextManager) -> None:
        messages = [
            {"role": "user", "content": "First"},
            {"role": "assistant", "content": "Reply 1"},
            {"role": "user", "content": "Second"},
            {"role": "assistant", "content": "Reply 2"},
        ]
        turns = manager._group_into_turns(messages)

        assert len(turns) == 2
        assert turns[0][0]["content"] == "First"
        assert turns[1][0]["content"] == "Second"
