"""Tests for app.ai.citation_mapper — mirrors citationMapper.test.ts."""
import pytest
from ai_citer.ai.citation_mapper import map_citations
from ai_citer.models import ClaudeFact, ClaudeCitation


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

def fact(text: str, quotes: list[str]) -> ClaudeFact:
    return ClaudeFact(fact=text, citations=[ClaudeCitation(exact_quote=q) for q in quotes])


# ---------------------------------------------------------------------------
# Tier 1: exact match
# ---------------------------------------------------------------------------

class TestExactMatch:
    def test_locates_verbatim_quote(self):
        raw = "The quick brown fox jumps over the lazy dog."
        quote = "brown fox jumps"
        [result] = map_citations(raw, [fact("test", [quote])])
        c = result.citations[0]
        assert c.charOffset == raw.index(quote)
        assert c.length == len(quote)
        assert c.confidence == "exact"
        assert c.quoteText == quote

    def test_matches_at_offset_zero(self):
        raw = "Starting words are important."
        quote = "Starting words"
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].charOffset == 0
        assert result.citations[0].confidence == "exact"

    def test_matches_at_end(self):
        raw = "Some text and then the end."
        quote = "the end."
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].charOffset == raw.index(quote)
        assert result.citations[0].confidence == "exact"

    def test_matches_first_occurrence(self):
        raw = "the cat sat. Later the cat ran."
        quote = "the cat"
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].charOffset == 0

    def test_preserves_special_characters(self):
        raw = "Revenue was $1.2M (up 15%) year-over-year."
        quote = "$1.2M (up 15%)"
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].confidence == "exact"
        assert result.citations[0].charOffset == raw.index(quote)

    def test_maps_multiple_citations_within_a_fact(self):
        raw = "Alpha is first. Beta is second. Gamma is third."
        [result] = map_citations(raw, [fact("test", ["Alpha is first.", "Gamma is third."])])
        assert result.citations[0].charOffset == 0
        assert result.citations[1].charOffset == raw.index("Gamma is third.")


# ---------------------------------------------------------------------------
# Tier 2: normalised match
# ---------------------------------------------------------------------------

class TestNormalizedMatch:
    def test_collapses_newline(self):
        raw = "First line\nSecond line of text here."
        quote = "First line Second line of text here."
        [result] = map_citations(raw, [fact("test", [quote])])
        c = result.citations[0]
        assert c.confidence == "normalized"
        assert c.charOffset == 0
        # quoteText should reflect the raw text, not the normalized version
        assert c.quoteText == "First line\nSecond line of text here."

    def test_collapses_multiple_spaces(self):
        raw = "The    wide    gap    here."
        quote = "The wide gap here."
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].confidence == "normalized"
        assert result.citations[0].charOffset == 0

    def test_case_insensitive(self):
        raw = "The Board approved the motion unanimously."
        quote = "the board approved the motion unanimously."
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].confidence == "normalized"
        assert result.citations[0].charOffset == 0

    def test_maps_offset_to_correct_raw_position(self):
        raw = "Intro text here.\n\nThe   key   finding   is   this."
        quote = "The key finding is this."
        [result] = map_citations(raw, [fact("test", [quote])])
        c = result.citations[0]
        assert c.confidence == "normalized"
        assert "The" in raw[c.charOffset: c.charOffset + c.length]


# ---------------------------------------------------------------------------
# Tier 3: fuzzy match
# ---------------------------------------------------------------------------

class TestFuzzyMatch:
    def test_matches_truncated_prefix(self):
        base = "The committee determined that all relevant parties must be notified within thirty days of the event."
        raw = base + " Additional text follows."
        quote = base + " XXXXX_NOT_IN_DOC_XXXXX"
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].confidence == "fuzzy"
        assert result.citations[0].charOffset == 0

    def test_returns_minus_one_for_short_unmatched_quote(self):
        raw = "Short quote test document with some content here."
        unmatched = "notpresent"  # 10 chars — below 60-char floor
        [r2] = map_citations(raw, [fact("test", [unmatched])])
        assert r2.citations[0].charOffset == -1

    def test_returns_minus_one_when_no_tier_matches(self):
        raw = "This document talks about apples and oranges."
        quote = "This document discusses quantum entanglement phenomena at very low temperatures indeed"
        [result] = map_citations(raw, [fact("test", [quote])])
        assert result.citations[0].charOffset == -1


# ---------------------------------------------------------------------------
# no-match fallback
# ---------------------------------------------------------------------------

class TestNoMatchFallback:
    def test_preserves_quote_text_when_unmatched(self):
        raw = "Some document text."
        quote = "completely fabricated text that does not appear in the document at all ever"
        [result] = map_citations(raw, [fact("test", [quote])])
        c = result.citations[0]
        assert c.charOffset == -1
        assert c.quoteText == quote

    def test_assigns_id_to_every_fact(self):
        raw = "Any document text here."
        [result] = map_citations(raw, [fact("a fact", ["Any document"])])
        assert isinstance(result.id, str)
        assert len(result.id) > 0


# ---------------------------------------------------------------------------
# multiple facts
# ---------------------------------------------------------------------------

class TestMultipleFacts:
    def test_one_fact_per_claude_fact(self):
        raw = "Revenue grew 20%. Headcount fell by 5%."
        facts = [
            fact("revenue", ["Revenue grew 20%."]),
            fact("headcount", ["Headcount fell by 5%."]),
        ]
        results = map_citations(raw, facts)
        assert len(results) == 2
        assert results[0].citations[0].charOffset == 0
        assert results[1].citations[0].charOffset == raw.index("Headcount")
