"""
Tests for cortex/hippo/summarize.py and cortex/hippo/quality.py.

Issue: #805
"""

from __future__ import annotations

import pytest
import pytest_asyncio

from cortex.hippo.config import HippoConfig
from cortex.hippo.quality import (
    QualityScoreResult,
    score_quality,
    _score_coherence,
    _score_compression,
    _score_coverage,
)
from cortex.hippo.summarize import (
    HippoSummarizer,
    SummarizeOptions,
    SummarizeResult,
)


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


@pytest.fixture
def summarizer_config() -> HippoConfig:
    return HippoConfig(
        enabled=True,
        summarizer_temperature=0.2,
        summarizer_aggressive_temperature=0.1,
        leaf_target_tokens=600,
        condensed_target_tokens=900,
        fallback_max_tokens=128,
    )


async def _good_complete_fn(**kwargs):
    """Mock LLM that returns a good summary."""
    return {
        "content": [
            {
                "type": "text",
                "text": (
                    "Decided to use PostgreSQL for the database layer. "
                    "Created user.py with authentication logic. "
                    "Set up Docker compose for local development. "
                    "Files: user.py created, docker-compose.yml modified. "
                    "Expand for details about: detailed auth flow, error handling patterns"
                ),
            }
        ]
    }


async def _empty_complete_fn(**kwargs):
    """Mock LLM that returns empty content."""
    return {"content": []}


async def _string_complete_fn(**kwargs):
    """Mock LLM that returns content as a plain string."""
    return {
        "content": "Summary text from string. Files: none. Expand for details about: stuff"
    }


async def _envelope_complete_fn(**kwargs):
    """Mock LLM that returns content in envelope fields."""
    return {"output_text": "Envelope summary. Files: none. Expand for details about: stuff"}


@pytest.fixture
def summarizer(summarizer_config):
    return HippoSummarizer(
        complete_fn=_good_complete_fn,
        config=summarizer_config,
    )


@pytest.fixture
def empty_summarizer(summarizer_config):
    return HippoSummarizer(
        complete_fn=_empty_complete_fn,
        config=summarizer_config,
    )


# ═══════════════════════════════════════════════════════════════════════════
# Quality scoring tests
# ═══════════════════════════════════════════════════════════════════════════


def test_quality_good_summary():
    """Well-compressed summary with footer gets a decent score."""
    input_text = (
        "We discussed the database architecture at length. "
        "Decided to use PostgreSQL for better JSON support. "
        "Created the user model in user.py with login, signup, and password reset. "
        "Set up Docker compose with postgres and redis services. "
        "Reviewed the authentication flow and decided on JWT tokens. "
        "Added rate limiting middleware to prevent abuse."
    ) * 5  # Repeat to make input substantial
    summary = (
        "Decided PostgreSQL for JSON support. Created user.py (login, signup, password reset). "
        "Docker compose: postgres + redis. JWT auth chosen. Rate limiting middleware added. "
        "Files: user.py created. "
        "Expand for details about: detailed discussion of alternatives, middleware config"
    )
    result = score_quality(input_text, summary)
    assert result.total >= 0.5
    assert 0 <= result.compression <= 1
    assert 0 <= result.coherence <= 1
    assert 0 <= result.coverage <= 1


def test_quality_verbatim_fails():
    """Summary identical to input scores poorly on compression."""
    long_text = "Some long conversation about databases and deployment " * 50
    result = score_quality(long_text, long_text)
    assert result.compression < 0.3


def test_quality_empty_summary_fails():
    """Empty summary gets very low score."""
    result = score_quality("Some input text about databases", "")
    assert result.total < 0.5


def test_quality_condensed_ranges():
    """Condensed mode uses different compression ideal ranges."""
    input_text = "Technical discussion " * 200
    # At 15% compression — should be good for condensed
    summary = "Technical discussion summary " * 30
    summary += "\n2026-03-15\nExpand for details about: things"
    result = score_quality(input_text, summary, is_condensed=True)
    assert result.compression >= 0.0  # Should be in acceptable range


def test_compression_too_short():
    """Extremely short summary gets penalized."""
    input_text = "Long text " * 200
    summary = "Ok."
    score, reason = _score_compression(input_text, summary, False)
    assert score < 0.5
    assert "Over-compressed" in reason


def test_compression_ideal():
    """Summary at 10% of input gets perfect compression score for leaf."""
    input_text = "a" * 4000  # ~1000 tokens
    summary = "a" * 400  # ~100 tokens = 10% ratio
    score, reason = _score_compression(input_text, summary, False)
    assert score == 1.0
    assert "ideal range" in reason


def test_coherence_with_footer():
    """Presence of expand footer boosts coherence."""
    summary = (
        "Some meaningful summary text. "
        "It has multiple sentences. "
        "And provides useful context. "
        "Expand for details about: dropped content"
    )
    score, reason = _score_coherence(summary, False)
    assert score >= 0.6
    assert "has expand footer" in reason


def test_coherence_meta_commentary_penalized():
    """Starting with 'This summary' is penalized."""
    summary = "This summary contains information about the conversation."
    score, reason = _score_coherence(summary, False)
    assert "meta-commentary" in reason


def test_coverage_all_terms():
    """Summary containing all key terms from input gets high coverage."""
    input_text = "PostgreSQL database migration schema deployment Docker kubernetes"
    summary = "PostgreSQL database migration schema deployment Docker kubernetes summary"
    score, reason = _score_coverage(input_text, summary)
    assert score >= 0.8


def test_coverage_no_terms():
    """Summary with zero key term overlap gets low coverage."""
    input_text = "PostgreSQL database migration schema deployment"
    summary = "Something completely unrelated about cooking recipes"
    score, reason = _score_coverage(input_text, summary)
    assert score < 0.3


# ═══════════════════════════════════════════════════════════════════════════
# Summarizer tests
# ═══════════════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_summarize_normal_mode(summarizer):
    """Normal mode produces a SummarizeResult with quality scoring."""
    result = await summarizer.summarize(
        "Some long text about databases and deployment " * 50
    )
    assert isinstance(result, SummarizeResult)
    assert result.content
    assert result.quality_score >= 0
    assert result.source == "content"
    assert result.mode_used == "normal"


@pytest.mark.asyncio
async def test_summarize_aggressive_mode(summarizer):
    """Aggressive mode uses lower temperature."""
    result = await summarizer.summarize(
        "Some text " * 50,
        aggressive=True,
    )
    assert result.mode_used == "aggressive"


@pytest.mark.asyncio
async def test_empty_completion_fallback(empty_summarizer):
    """When LLM returns empty content, falls back to truncation."""
    result = await empty_summarizer.summarize(
        "Some text to summarize that should trigger fallback " * 20
    )
    assert result.source == "fallback"
    assert len(result.content) > 0
    assert "[Truncated" in result.content


@pytest.mark.asyncio
async def test_envelope_extraction():
    """Envelope-level fields are extracted when content array is empty."""
    config = HippoConfig(enabled=True, fallback_max_tokens=128)
    summarizer = HippoSummarizer(
        complete_fn=_envelope_complete_fn,
        config=config,
    )
    # The _envelope_complete_fn returns {"output_text": "..."} but empty content array
    # In this case the normalizer should find the envelope field
    result = summarizer._normalize_completion({"output_text": "Envelope text"})
    assert result == "Envelope text"


@pytest.mark.asyncio
async def test_string_content_normalized():
    """Content as a plain string (not array) is handled."""
    config = HippoConfig(enabled=True, fallback_max_tokens=128)
    summarizer = HippoSummarizer(
        complete_fn=_string_complete_fn,
        config=config,
    )
    result = summarizer._normalize_completion({"content": "Plain string content"})
    assert result == "Plain string content"


# ═══════════════════════════════════════════════════════════════════════════
# Prompt building tests
# ═══════════════════════════════════════════════════════════════════════════


def test_leaf_prompt_has_required_sections(summarizer):
    """Built leaf prompt contains required elements."""
    prompt = summarizer._build_prompt("test text", "normal", 300, SummarizeOptions())
    assert "conversation_segment" in prompt
    assert "Expand for details about:" in prompt
    assert "Files:" in prompt.lower() or "file operations" in prompt.lower()


def test_aggressive_prompt_different(summarizer):
    """Aggressive prompt uses different policy wording."""
    normal = summarizer._build_prompt("text", "normal", 300, SummarizeOptions())
    aggressive = summarizer._build_prompt("text", "aggressive", 300, SummarizeOptions())
    assert "Normal summary policy" in normal
    assert "Aggressive summary policy" in aggressive


def test_condensed_d1_prompt(summarizer):
    """Condensed depth=1 uses D1 prompt."""
    options = SummarizeOptions(is_condensed=True, depth=1)
    prompt = summarizer._build_prompt("text", "normal", 500, options)
    assert "leaf-level conversation summaries" in prompt
    assert "conversation_segment" not in prompt


def test_condensed_d2_prompt(summarizer):
    """Condensed depth=2 uses D2 prompt."""
    options = SummarizeOptions(is_condensed=True, depth=2)
    prompt = summarizer._build_prompt("text", "normal", 500, options)
    assert "session-level summaries" in prompt


def test_condensed_d3_prompt(summarizer):
    """Condensed depth>=3 uses D3+ prompt."""
    options = SummarizeOptions(is_condensed=True, depth=4)
    prompt = summarizer._build_prompt("text", "normal", 500, options)
    assert "high-level memory node" in prompt


def test_previous_context_included(summarizer):
    """Previous summary context is injected into prompt."""
    options = SummarizeOptions(previous_summary="Prior context here")
    prompt = summarizer._build_prompt("text", "normal", 300, options)
    assert "Prior context here" in prompt
    assert "previous_context" in prompt


def test_custom_instructions_included(summarizer):
    """Custom instructions are included in prompt."""
    options = SummarizeOptions(custom_instructions="Always preserve Python code blocks")
    prompt = summarizer._build_prompt("text", "normal", 300, options)
    assert "Always preserve Python code blocks" in prompt


# ═══════════════════════════════════════════════════════════════════════════
# Target token resolution
# ═══════════════════════════════════════════════════════════════════════════


def test_normal_target_tokens(summarizer):
    """Normal mode: max(192, min(1200, floor(input * 0.35)))."""
    target = summarizer._resolve_target_tokens(1000, "normal", False)
    assert target == max(192, min(1200, int(1000 * 0.35)))  # 350


def test_aggressive_target_tokens(summarizer):
    """Aggressive mode returns lower target than normal."""
    normal = summarizer._resolve_target_tokens(1000, "normal", False)
    aggressive = summarizer._resolve_target_tokens(1000, "aggressive", False)
    assert aggressive < normal


def test_condensed_target_tokens(summarizer):
    """Condensed mode uses config.condensed_target_tokens."""
    target = summarizer._resolve_target_tokens(1000, "normal", True)
    assert target == max(512, summarizer.config.condensed_target_tokens)


def test_aggressive_small_input(summarizer):
    """Aggressive mode on small input respects minimum of 96."""
    target = summarizer._resolve_target_tokens(100, "aggressive", False)
    assert target >= 96


def test_normal_large_input(summarizer):
    """Normal mode on large input caps at 1200."""
    target = summarizer._resolve_target_tokens(100_000, "normal", False)
    assert target == 1200
