"""
Concurrent compaction + search stress tests.

Verifies that grep, describe/expand, and concurrent compactions work correctly
while compaction is running. The goal is no crashes or corrupt results —
exact result counts may vary due to concurrent data changes.

Issue: #1225
"""

from __future__ import annotations

import asyncio

import pytest
import pytest_asyncio

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


# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════


async def _mock_summarize(text: str, aggressive: bool = False, options=None) -> SummarizeResult:
    """Deterministic mock summarizer for compaction."""
    await asyncio.sleep(0.01)  # Simulate small latency
    content = f"Summary of {len(text)} chars. Expand for details about: dropped content"
    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",
    )


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


@pytest.fixture
def stress_config() -> HippoConfig:
    """Config tuned for stress testing: low thresholds to trigger compaction easily."""
    return HippoConfig(
        enabled=True,
        db_path=":memory:",
        fresh_tail_count=4,
        max_tokens=5_000,
        context_threshold=0.3,  # Low to trigger compaction quickly
        context_window_tokens=200,
        leaf_trigger_margin=50,
        leaf_chunk_tokens=200,
        leaf_min_fanout=2,
        condensed_min_fanout=2,
        condensed_min_fanout_hard=2,
        condensed_min_chunk_tokens=50,
        max_rounds=3,
        fallback_max_tokens=128,
        enable_embeddings=False,  # No external embedding calls
        hybrid_search_enabled=True,
        vec_enabled=False,
    )


@pytest_asyncio.fixture
async def stress_engine(stress_config: HippoConfig) -> HippoEngine:
    eng = HippoEngine(config=stress_config)
    await eng.initialize(
        db_path=":memory:",
        owner_id="stress-owner",
        conversation_id="stress-session",
    )
    yield eng
    await eng.shutdown()


async def _populate_messages(engine: HippoEngine, count: int = 40) -> None:
    """Ingest many messages to build up compactable content."""
    for i in range(count):
        role = "user" if i % 2 == 0 else "assistant"
        content = (
            f"Message {i}: This is a conversation about building a web application "
            f"with React and Python. We are discussing architecture decisions, "
            f"database schema design, and deployment strategy for iteration {i}."
        )
        await engine.ingest_message(role, content)


# ═══════════════════════════════════════════════════════════════════════════
# Tests
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_concurrent_compaction_and_grep(stress_engine: HippoEngine):
    """Compaction running concurrently with grep searches produces no crashes."""
    await _populate_messages(stress_engine, count=40)

    compaction_engine = CompactionEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )

    retrieval = RetrievalEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )
    conv_id = stress_engine._conversation.conversation_id

    errors: list[Exception] = []

    async def do_compaction():
        try:
            await compaction_engine.compact_full_sweep(
                conversation_id=conv_id,
                token_budget=5_000,
                summarize=_mock_summarize,
                force=True,
            )
        except Exception as e:
            errors.append(e)

    async def do_grep_searches():
        try:
            for pattern in ["architecture", "database", "React", "deploy", "Message"]:
                result = await retrieval.grep(
                    GrepInput(
                        conversation_id=conv_id,
                        owner_id="stress-owner",
                        pattern=pattern,
                        limit=10,
                    )
                )
                # Results may vary due to concurrent compaction, but no exceptions
                assert result is not None
        except Exception as e:
            errors.append(e)

    # Run compaction and grep concurrently
    await asyncio.gather(
        do_compaction(),
        do_grep_searches(),
        do_grep_searches(),  # Multiple concurrent grep tasks
    )

    assert len(errors) == 0, f"Concurrent errors: {errors}"


@pytest.mark.asyncio
async def test_concurrent_compaction_and_grep_full_text(stress_engine: HippoEngine):
    """Full-text grep during compaction returns consistent results."""
    await _populate_messages(stress_engine, count=40)

    compaction_engine = CompactionEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )

    retrieval = RetrievalEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )
    conv_id = stress_engine._conversation.conversation_id

    errors: list[Exception] = []

    async def do_compaction():
        try:
            await compaction_engine.compact_full_sweep(
                conversation_id=conv_id,
                token_budget=5_000,
                summarize=_mock_summarize,
                force=True,
            )
        except Exception as e:
            errors.append(e)

    async def do_fulltext_grep():
        try:
            for query in ["web application", "database schema", "deployment strategy"]:
                result = await retrieval.grep(
                    GrepInput(
                        conversation_id=conv_id,
                        owner_id="stress-owner",
                        pattern=query,
                        mode="full_text",
                        limit=10,
                    )
                )
                assert result is not None
        except Exception as e:
            errors.append(e)

    await asyncio.gather(
        do_compaction(),
        do_fulltext_grep(),
        do_fulltext_grep(),
    )

    assert len(errors) == 0, f"Concurrent errors: {errors}"


@pytest.mark.asyncio
async def test_concurrent_compaction_and_dag_traversal(stress_engine: HippoEngine):
    """Describe + expand during compaction — DAG traversal is safe."""
    await _populate_messages(stress_engine, count=40)

    compaction_engine = CompactionEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )
    conv_id = stress_engine._conversation.conversation_id

    # Run an initial compaction to create summaries we can describe/expand
    await compaction_engine.compact_full_sweep(
        conversation_id=conv_id,
        token_budget=5_000,
        summarize=_mock_summarize,
        force=True,
    )

    # Collect summary IDs for describe/expand
    summaries = await stress_engine.summary_store.get_summaries_for_conversation(
        conv_id, "stress-owner"
    )
    summary_ids = [s.summary_id for s in summaries[:5]]

    if not summary_ids:
        pytest.skip("No summaries created by initial compaction")

    # Add more messages so a second compaction has work to do
    await _populate_messages(stress_engine, count=20)

    errors: list[Exception] = []

    async def do_compaction():
        try:
            await compaction_engine.compact_full_sweep(
                conversation_id=conv_id,
                token_budget=5_000,
                summarize=_mock_summarize,
                force=True,
            )
        except Exception as e:
            errors.append(e)

    async def do_describe_expand():
        try:
            for sid in summary_ids:
                # Describe
                summary = await stress_engine.summary_store.get_summary("stress-owner", sid)
                if summary:
                    assert summary.content is not None
                    assert summary.summary_id == sid

                # Get source message IDs (expand/DAG traversal)
                msg_ids = await stress_engine.summary_store.get_source_message_ids(sid)
                # msg_ids may be empty if compaction changed things, but should not crash
                assert isinstance(msg_ids, list)
        except Exception as e:
            errors.append(e)

    await asyncio.gather(
        do_compaction(),
        do_describe_expand(),
        do_describe_expand(),
    )

    assert len(errors) == 0, f"Concurrent errors: {errors}"


@pytest.mark.asyncio
async def test_concurrent_multiple_compactions(stress_engine: HippoEngine):
    """Multiple concurrent compaction attempts don't corrupt data."""
    await _populate_messages(stress_engine, count=60)

    compaction_engine = CompactionEngine(
        conversation_store=stress_engine.conversation_store,
        summary_store=stress_engine.summary_store,
        config=stress_engine.config,
    )
    conv_id = stress_engine._conversation.conversation_id

    successes = 0
    sqlite_errors = 0
    other_errors: list[Exception] = []

    async def do_compaction(label: str):
        nonlocal successes, sqlite_errors
        try:
            await compaction_engine.compact_full_sweep(
                conversation_id=conv_id,
                token_budget=5_000,
                summarize=_mock_summarize,
                force=True,
            )
            successes += 1
        except Exception as e:
            # SQLite transaction contention is expected with concurrent writes
            if "transaction" in str(e).lower() or "database is locked" in str(e).lower():
                sqlite_errors += 1
            else:
                other_errors.append(e)

    # Fire 3 compactions concurrently
    await asyncio.gather(
        do_compaction("A"),
        do_compaction("B"),
        do_compaction("C"),
    )

    # At least one should succeed; others may hit SQLite contention
    assert successes >= 1, "At least one compaction should succeed"
    assert len(other_errors) == 0, f"Unexpected errors: {other_errors}"

    # Verify data integrity: context items should be coherent
    items = await stress_engine.summary_store.get_context_items(conv_id)
    assert len(items) > 0, "Context items should exist after compaction"

    # Ordinals should be monotonically non-decreasing
    ordinals = [item.ordinal for item in items]
    for i in range(1, len(ordinals)):
        assert ordinals[i] >= ordinals[i - 1], (
            f"Ordinals not monotonic: {ordinals[i-1]} > {ordinals[i]}"
        )
