"""
End-to-end integration tests for Hippo LCM with real SQLite.

Full lifecycle: bootstrap → ingest → compact → assemble → expand → verify.

Issue: #1124
"""

from __future__ import annotations

import pytest
import pytest_asyncio

from cortex.hippo.compaction import CompactionEngine, estimate_tokens
from cortex.hippo.config import HippoConfig
from cortex.hippo.engine import HippoEngine
from cortex.hippo.models import ContextItemType
from cortex.hippo.retrieval import ExpandInput, GrepInput, RetrievalEngine
from cortex.hippo.summarize import SummarizeResult


# ═══════════════════════════════════════════════════════════════════════════
# Fixtures
# ═══════════════════════════════════════════════════════════════════════════


@pytest.fixture
def integration_config() -> HippoConfig:
    """Config for integration tests — low thresholds for fast compaction."""
    return HippoConfig(
        enabled=True,
        db_path=":memory:",
        fresh_tail_count=4,
        max_tokens=10_000,
        context_threshold=0.3,  # Very low so compaction fires easily
        context_window_tokens=100,
        leaf_trigger_margin=10,
        leaf_chunk_tokens=300,
        leaf_target_tokens=100,
        condensed_target_tokens=150,
        leaf_min_fanout=2,
        condensed_min_fanout=2,
        condensed_min_fanout_hard=2,
        condensed_min_chunk_tokens=20,
        max_rounds=10,
        fallback_max_tokens=128,
        incremental_condensed_max_depth=0,
        auto_compaction_summary=True,
    )


async def _mock_summarize(text: str, aggressive: bool, options=None) -> SummarizeResult:
    """Deterministic mock summarizer for integration tests."""
    # Preserve some keywords for grep verification
    keywords = []
    for word in ["deploy", "config", "database", "migration", "refactor", "bugfix"]:
        if word in text.lower():
            keywords.append(word)

    kw_str = ", ".join(keywords) if keywords else "general discussion"
    content = (
        f"Summary covering {kw_str}. "
        f"Compressed from {len(text)} chars. "
        f"Files: none. "
        f"Expand for details about: {kw_str}"
    )
    return SummarizeResult(
        content=content,
        quality_score=0.8,
        quality_breakdown={"compression": 0.8, "coherence": 0.8, "coverage": 0.8},
        source="content",
        mode_used="aggressive" if aggressive else "normal",
    )


def _make_messages(n: int, include_tool_calls: bool = False) -> list[tuple[str, str]]:
    """Generate N (role, content) pairs for ingestion."""
    messages = []
    topics = [
        "Let's deploy the new config changes to production",
        "The database migration ran successfully with zero downtime",
        "I've started the refactor of the authentication module",
        "There's a bugfix needed for the session handling code",
        "The deploy pipeline needs a new staging environment",
        "Config values should be loaded from environment variables",
        "Database indexes need optimization for the search queries",
        "The migration script handles both up and down directions",
    ]

    for i in range(n):
        role = "user" if i % 2 == 0 else "assistant"
        topic = topics[i % len(topics)]
        content = f"[Turn {i}] {topic} — additional context padding " + "x" * 60
        messages.append((role, content))

        if include_tool_calls and i % 7 == 3:
            # Add a tool response every 7th message
            messages.append(("tool", f"Tool output for turn {i}: success"))

    return messages


# ═══════════════════════════════════════════════════════════════════════════
# Full Lifecycle Test
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_full_lifecycle(integration_config):
    """bootstrap → ingest 50 messages → compact → assemble → expand → verify."""
    # 1. Create engine with in-memory DB
    engine = HippoEngine(config=integration_config, summarize_fn=_mock_summarize)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="integration-owner",
        conversation_id="lifecycle-session",
    )

    try:
        # 2. Session bootstrapped
        assert conv.conversation_id > 0

        # 3. Ingest 50 messages (mix of user/assistant, some with tool calls)
        msg_pairs = _make_messages(50, include_tool_calls=True)
        for role, content in msg_pairs:
            await engine.ingest_message(role, content)

        stats_before = await engine.get_stats()
        assert stats_before.message_count == len(msg_pairs)

        # 4. Trigger leaf compaction
        ce = CompactionEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
            config=integration_config,
        )

        result = await ce.compact_full_sweep(
            conversation_id=conv.conversation_id,
            token_budget=integration_config.max_tokens,
            summarize=_mock_summarize,
            force=True,
        )

        # 5. Verify summaries created in DB
        assert result.action_taken is True
        assert len(result.created_summary_ids) >= 1

        stats_after = await engine.get_stats()
        assert stats_after.summary_count >= 1

        # Verify summaries have correct structure
        for sid in result.created_summary_ids:
            summary = await engine.summary_store.get_summary("integration-owner", sid)
            assert summary is not None
            assert summary.summary_id == sid
            assert summary.depth >= 0
            assert summary.token_count > 0
            assert len(summary.content) > 0

        # 6. Assemble context — should include summaries + recent messages
        assembled = await engine.get_context()
        assert assembled.total_tokens > 0
        assert len(assembled.messages) > 0

        has_summary = any(i.kind == ContextItemType.SUMMARY for i in assembled.items)
        has_message = any(i.kind == ContextItemType.MESSAGE for i in assembled.items)
        assert has_summary, "Assembled context should include summaries"
        assert has_message, "Assembled context should include messages"

        # Fresh tail should be present
        message_items = [i for i in assembled.items if i.kind == ContextItemType.MESSAGE]
        assert len(message_items) >= integration_config.fresh_tail_count

        # 7. Grep for a known phrase — should find it in summary
        retrieval = RetrievalEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
        )

        grep_result = await retrieval.grep(GrepInput(
            pattern="deploy",
            mode="regex",
            scope="both",
            conversation_id=conv.conversation_id,
            owner_id="integration-owner",
        ))
        assert grep_result.total_matches > 0, "Grep should find 'deploy' in messages or summaries"

        # 8. Describe a summary — should return metadata
        sid = result.created_summary_ids[0]
        desc = await retrieval.describe(id=sid, owner_id="integration-owner")
        assert desc is not None
        assert desc.id == sid
        assert desc.type == "summary"

        # 9. Expand a summary — should return content
        expand_result = await retrieval.expand(ExpandInput(
            summary_id=sid,
            depth=3,
            include_messages=True,
        ))
        assert expand_result is not None

        # 10. Verify context items changed (compacted messages replaced by summaries)
        ctx_items = await engine.summary_store.get_context_items(conv.conversation_id)
        summary_items = [i for i in ctx_items if i.item_type == "summary"]
        assert len(summary_items) >= 1, "Context items should include summary references"

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# Below-Threshold: No Compaction
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_lifecycle_no_compaction_below_threshold(integration_config):
    """5 messages — no compaction triggered when under threshold."""
    engine = HippoEngine(config=integration_config)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="no-compact-session",
    )

    try:
        # Ingest only 5 messages
        for i in range(5):
            role = "user" if i % 2 == 0 else "assistant"
            await engine.ingest_message(role, f"Short message {i}")

        stats = await engine.get_stats()
        assert stats.message_count == 5
        assert stats.summary_count == 0

        # Try compaction without force — should not fire (messages too short)
        ce = CompactionEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
            config=integration_config,
        )

        # Evaluate threshold
        decision = await ce.evaluate(
            conversation_id=conv.conversation_id,
            token_budget=integration_config.max_tokens,
        )

        # With short messages, shouldn't exceed threshold
        # (5 short messages × ~10 tokens each = ~50 tokens << 10000 * 0.3 = 3000)
        assert decision.current_tokens < decision.threshold or not decision.should_compact

        # Assembly should return raw messages only
        assembled = await engine.get_context()
        assert assembled.summary_tokens == 0
        assert len(assembled.messages) == 5

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# Multiple Compactions
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_lifecycle_multiple_compactions(integration_config):
    """100 messages → multiple leaf passes via full sweep."""
    engine = HippoEngine(config=integration_config, summarize_fn=_mock_summarize)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="multi-compact-session",
    )

    try:
        # Ingest 100 messages with substantial content
        msg_pairs = _make_messages(100, include_tool_calls=True)
        for role, content in msg_pairs:
            await engine.ingest_message(role, content)

        stats_before = await engine.get_stats()
        assert stats_before.message_count == len(msg_pairs)

        ce = CompactionEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
            config=integration_config,
        )

        # Full sweep should do multiple rounds
        result = await ce.compact_full_sweep(
            conversation_id=conv.conversation_id,
            token_budget=integration_config.max_tokens,
            summarize=_mock_summarize,
            force=True,
        )

        assert result.action_taken is True
        assert result.rounds >= 2, f"Expected multiple rounds for 100 messages, got {result.rounds}"
        assert len(result.created_summary_ids) >= 2

        # Verify all summaries are valid
        for sid in result.created_summary_ids:
            summary = await engine.summary_store.get_summary("test-owner", sid)
            assert summary is not None, f"Summary {sid} not found in store"

        # Assembly should work with compacted context
        assembled = await engine.get_context()
        assert assembled.total_tokens > 0
        assert assembled.total_tokens <= integration_config.max_tokens

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# Condensation After Many Leaves
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_lifecycle_condensed_after_many_leaves(integration_config):
    """Enough leaf summaries → condensation pass triggers."""
    # Enable incremental condensed and lower fanout requirements
    config = integration_config.model_copy(update={
        "incremental_condensed_max_depth": -1,  # Unlimited
        "condensed_min_fanout": 2,
        "condensed_min_fanout_hard": 2,
        "condensed_min_chunk_tokens": 10,
        "leaf_chunk_tokens": 150,  # Smaller chunks = more summaries
    })

    engine = HippoEngine(config=config, summarize_fn=_mock_summarize)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="condensed-session",
    )

    try:
        # Ingest many messages to generate multiple leaf summaries
        msg_pairs = _make_messages(60, include_tool_calls=False)
        for role, content in msg_pairs:
            await engine.ingest_message(role, content)

        ce = CompactionEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
            config=config,
        )

        # Run full sweep with hard trigger to force condensation
        result = await ce.compact_full_sweep(
            conversation_id=conv.conversation_id,
            token_budget=config.max_tokens,
            summarize=_mock_summarize,
            force=True,
            hard_trigger=True,
        )

        assert result.action_taken is True

        # Check if condensation happened
        if result.condensed:
            # Verify condensed summary exists at depth > 0
            found_condensed = False
            for sid in result.created_summary_ids:
                summary = await engine.summary_store.get_summary("test-owner", sid)
                if summary and summary.kind == "condensed" and summary.depth >= 1:
                    found_condensed = True
                    break
            assert found_condensed, "Should have a condensed summary at depth >= 1"
        else:
            # Even without condensation, multiple leaf summaries should exist
            stats = await engine.get_stats()
            assert stats.summary_count >= 2, "Should have multiple leaf summaries"

        # Verify assembly still works after condensation
        assembled = await engine.get_context()
        assert assembled.total_tokens > 0
        assert len(assembled.messages) > 0

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# After-Turn Integration
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_after_turn_ingest_and_compaction(integration_config):
    """after_turn ingests new messages and may trigger compaction."""
    config = integration_config.model_copy(update={
        "context_window_tokens": 100,
        "leaf_trigger_margin": 10,
    })

    engine = HippoEngine(config=config, summarize_fn=_mock_summarize)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="after-turn-session",
    )

    try:
        # Build a message list simulating gateway messages
        messages = []
        for i in range(20):
            role = "user" if i % 2 == 0 else "assistant"
            messages.append({"role": role, "content": f"Turn {i}: padding " + "x" * 80})

        # Call after_turn with all messages
        await engine.after_turn(
            conversation_id=conv.conversation_id,
            messages=messages,
            pre_prompt_count=0,
            token_budget=config.max_tokens,
        )

        # Messages should be ingested
        stats = await engine.get_stats()
        assert stats.message_count >= 15  # Some messages ingested (may vary)

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# Heartbeat Pruning Integration
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_heartbeat_pruning(integration_config):
    """Heartbeat OK turns are pruned during after_turn."""
    engine = HippoEngine(config=integration_config)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="heartbeat-session",
    )

    try:
        messages = [
            {"role": "user", "content": "Real question"},
            {"role": "assistant", "content": "Real answer"},
            {"role": "user", "content": "heartbeat check"},
            {"role": "assistant", "content": "HEARTBEAT_OK"},
            {"role": "user", "content": "Another question"},
            {"role": "assistant", "content": "Another answer"},
        ]

        await engine.after_turn(
            conversation_id=conv.conversation_id,
            messages=messages,
            pre_prompt_count=0,
            is_heartbeat=True,
        )

        stats = await engine.get_stats()
        # Heartbeat pair should be pruned — 4 messages instead of 6
        assert stats.message_count == 4

    finally:
        await engine.shutdown()


# ═══════════════════════════════════════════════════════════════════════════
# Stats Verification
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_stats_accuracy(integration_config):
    """Stats reflect actual state of messages and summaries."""
    engine = HippoEngine(config=integration_config, summarize_fn=_mock_summarize)
    conv = await engine.initialize(
        db_path=":memory:",
        owner_id="test-owner",
        conversation_id="stats-session",
    )

    try:
        # Empty state
        stats = await engine.get_stats()
        assert stats.message_count == 0
        assert stats.summary_count == 0
        assert stats.max_depth == 0

        # After ingestion
        for i in range(10):
            role = "user" if i % 2 == 0 else "assistant"
            await engine.ingest_message(role, f"Message {i}: " + "y" * 60)

        stats = await engine.get_stats()
        assert stats.message_count == 10
        assert stats.total_message_tokens > 0

        # After compaction
        ce = CompactionEngine(
            conversation_store=engine.conversation_store,
            summary_store=engine.summary_store,
            config=integration_config,
        )
        await ce.compact_full_sweep(
            conversation_id=conv.conversation_id,
            token_budget=integration_config.max_tokens,
            summarize=_mock_summarize,
            force=True,
        )

        stats = await engine.get_stats()
        assert stats.summary_count >= 1
        assert stats.total_summary_tokens > 0

    finally:
        await engine.shutdown()
