"""Tests for ContextPacker — context assembly within token budgets."""

import sqlite3
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

from memotrail.core.context_packer import ContextPacker
from memotrail.storage.sqlite_store import SQLiteStore
from memotrail.utils.tokens import count_tokens


def _make_sqlite(tmp_path: Path) -> SQLiteStore:
    """Create a fresh SQLiteStore in a temp directory."""
    store = SQLiteStore(db_path=tmp_path / "test.db")
    store.connect()
    return store


class TestPackProjectContext:
    def test_empty_db_returns_no_context(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("myproject")
        assert "No context available" in result

    def test_includes_decisions(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        sqlite.add_decision(session.id, "Use PostgreSQL for data storage", category="technology")
        sqlite.add_decision(session.id, "Use Redis for caching", category="technology")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1")
        assert "Decisions" in result
        assert "PostgreSQL" in result
        assert "Redis" in result

    def test_includes_memories(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        sqlite.save_memory("Always run tests before committing", tags=["workflow"])

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1")
        assert "Memories" in result
        assert "tests before committing" in result

    def test_includes_session_summaries(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        sqlite.update_session(session.id, summary="Implemented user auth with JWT", message_count=10)

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1")
        assert "Recent Sessions" in result
        assert "JWT" in result

    def test_respects_budget(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        for i in range(50):
            sqlite.add_decision(session.id, f"Decision number {i} about topic {i} with extra details", category="general")
        for i in range(20):
            sqlite.save_memory(f"Memory item number {i} with extra content for padding")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1", budget=200)
        tokens = count_tokens(result)
        assert tokens <= 250  # some overhead for headers

    def test_header_includes_project_name(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="my-app")
        sqlite.add_decision(session.id, "Use React for the frontend", category="technology")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("my-app")
        assert "my-app" in result

    def test_filters_decisions_by_project(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        s1 = sqlite.create_session(project="proj1")
        s2 = sqlite.create_session(project="proj2")
        sqlite.add_decision(s1.id, "Use FastAPI for proj1", category="technology")
        sqlite.add_decision(s2.id, "Use Django for proj2", category="technology")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1")
        assert "FastAPI" in result
        assert "Django" not in result


class TestPackQueryContext:
    def test_no_results_returns_message(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = []
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("How does auth work?")
        assert "No relevant past context" in result

    def test_includes_search_results(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)

        mock_result = MagicMock()
        mock_result.chunk_text = "We implemented OAuth2 with JWT tokens for authentication."
        mock_result.session_id = "sess_abc"
        mock_result.project = "myapp"
        mock_result.score = 0.85
        mock_result.timestamp = "2024-01-01"

        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = [mock_result]
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("authentication")
        assert "Relevant Conversations" in result
        assert "OAuth2" in result

    def test_includes_memory_results(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)

        mock_memory = MagicMock()
        mock_memory.text = "We use bcrypt for password hashing"
        mock_memory.score = 0.90
        mock_memory.chunk_id = "mem_123"
        mock_memory.metadata = {}

        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = []
        mock_searcher.search_memories.return_value = [mock_memory]

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("password security")
        assert "Relevant Memories" in result
        assert "bcrypt" in result

    def test_respects_budget(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)

        mock_results = []
        for i in range(20):
            r = MagicMock()
            r.chunk_text = f"Long conversation chunk {i} " * 50
            r.session_id = f"sess_{i}"
            r.project = "proj"
            r.score = 0.9 - i * 0.01
            r.timestamp = "2024-01-01"
            mock_results.append(r)

        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = mock_results
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("test", budget=500)
        tokens = count_tokens(result)
        assert tokens <= 600  # some overhead

    def test_with_project_filter(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        s1 = sqlite.create_session(project="filtered_proj")
        sqlite.add_decision(s1.id, "Use TypeScript for filtered_proj", category="technology")

        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = []
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("typescript", project="filtered_proj")
        assert "TypeScript" in result

    def test_includes_decisions_in_query_context(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        sqlite.add_decision(session.id, "Use SQLite for metadata storage", category="technology")

        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = []
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("database choice", project="proj1")
        assert "Decisions" in result
        assert "SQLite" in result


class TestBudgetAllocation:
    def test_tiny_budget_still_produces_output(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        sqlite.add_decision(session.id, "Use Python", category="technology")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1", budget=50)
        # Should at least have the header or a minimal response
        assert len(result) > 0

    def test_large_budget_includes_all_sections(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project="proj1")
        sqlite.add_decision(session.id, "Use React for frontend", category="technology")
        sqlite.save_memory("Always lint before committing")
        sqlite.update_session(session.id, summary="Set up project scaffolding", message_count=5)

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        result = packer.pack_project_context("proj1", budget=100000)
        assert "Decisions" in result
        assert "Memories" in result
        assert "Recent Sessions" in result

    def test_default_budget_from_config(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        # Just verify it doesn't crash with default budget
        result = packer.pack_project_context("proj1")
        assert isinstance(result, str)


class TestEdgeCases:
    def test_none_project_decisions(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        session = sqlite.create_session(project=None)
        sqlite.add_decision(session.id, "Global decision about testing", category="approach")

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[]),
                               search_memories=MagicMock(return_value=[])),
        )
        result = packer.pack_query_context("testing")
        assert "testing" in result.lower() or "Decisions" in result

    def test_empty_query(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        mock_searcher = MagicMock()
        mock_searcher.search_chats.return_value = []
        mock_searcher.search_memories.return_value = []

        packer = ContextPacker(sqlite_store=sqlite, searcher=mock_searcher)
        result = packer.pack_query_context("")
        assert isinstance(result, str)

    def test_session_summary_without_project_filter(self, tmp_path):
        sqlite = _make_sqlite(tmp_path)
        s1 = sqlite.create_session(project="proj1")
        s2 = sqlite.create_session(project="proj2")
        sqlite.update_session(s1.id, summary="Summary A", message_count=5)
        sqlite.update_session(s2.id, summary="Summary B", message_count=3)

        packer = ContextPacker(
            sqlite_store=sqlite,
            searcher=MagicMock(search_chats=MagicMock(return_value=[])),
        )
        # When querying without project filter (pack_query_context with no project)
        # pack_project_context still requires a project
        result = packer.pack_project_context("proj1")
        assert "Summary A" in result
