"""Tests for warrant-lite — anchor-class hard check.

Covers:
  - is_relation_question detects "who is X's Y", "who founded Z",
    "who is the boss of W", etc.
  - extract_answer_anchors pulls multi-word proper nouns;
    falls back to solo caps skipping sentence-starter
  - extract_date_anchors pulls 4-digit years (1500-2199 range)
  - warrant_check composes proper-noun + date anchor checks:
      * proper-noun: gated on relation question shape; any-match
        across anchors
      * date: always-on when claim has a year; ALL years required
        in some cited span
      * vacuous-pass when neither class fires
"""

from __future__ import annotations

from aborist.qa.warrant import (
    _question_is_count_shape,
    _question_is_entity_list_shape,
    _question_is_why_shape,
    extract_answer_anchors,
    extract_cause_anchors,
    extract_count_anchors,
    extract_date_anchors,
    is_relation_question,
    warrant_check,
)


# ---------------------------------------------------------------- is_relation_question


def test_is_relation_question_possessive_who():
    assert is_relation_question("who is homer simpson's boss?")
    assert is_relation_question("who is bilbo baggins's nephew?")
    assert is_relation_question("who is supermans girlfriend?")


def test_is_relation_question_who_action_verb():
    assert is_relation_question("who founded microsoft?")
    assert is_relation_question("who painted the mona lisa?")
    assert is_relation_question("who wrote hamlet?")
    assert is_relation_question("who directed jaws?")
    assert is_relation_question("who invented the doppler effect?")


def test_is_relation_question_relation_of():
    assert is_relation_question("the founder of microsoft")
    assert is_relation_question("the boss of homer simpson")
    assert is_relation_question("girlfriend of superman")


def test_is_relation_question_not_a_relation():
    """Broad-descriptive questions are NOT relation lookups."""
    assert not is_relation_question("tell me about connecticut")
    assert not is_relation_question("plot of red fish blue fish?")
    assert not is_relation_question("what is starcraft?")


def test_is_relation_question_handles_empty_or_none():
    assert not is_relation_question("")
    assert not is_relation_question(None)


# ---------------------------------------------------------------- extract_answer_anchors


def test_extract_anchors_finds_multiword_proper_nouns():
    anchors = extract_answer_anchors("Homer Simpson's boss is Mr. Burns.")
    # Multi-word proper-noun phrases captured.
    assert "Homer Simpson" in anchors
    assert "Mr. Burns" in anchors


def test_extract_anchors_finds_lois_lane():
    anchors = extract_answer_anchors("Superman's girlfriend is Lois Lane.")
    assert "Lois Lane" in anchors


def test_extract_anchors_falls_back_to_solo_caps_skipping_sentence_start():
    """No multi-word phrases — falls back to solo caps but skips
    the sentence-starter to avoid 'Connecticut is a state...' false
    positives where the leading capital isn't a special-name anchor."""
    anchors = extract_answer_anchors("Connecticut is in New England region.")
    # Multi-word "New England" SHOULD be captured (region phrase).
    # Connecticut as the leading word + standalone is NOT a strong anchor.
    assert any("New England" in a for a in anchors)


def test_extract_anchors_empty_when_no_proper_nouns():
    """A claim with no capitalized words → no anchors."""
    anchors = extract_answer_anchors("the boy and the girl have many fish.")
    assert anchors == []


def test_extract_anchors_handles_empty_text():
    assert extract_answer_anchors("") == []
    assert extract_answer_anchors(None) == []


# ---------------------------------------------------------------- warrant_check


def test_warrant_passes_when_anchor_in_span():
    """Relation-question happy path: claim names Mr. Burns, span has Mr. Burns."""
    ok, missing = warrant_check(
        "Homer Simpson's boss is Mr. Burns.",
        ["Mr. Burns owns the Springfield Nuclear Power Plant where Homer works."],
        question="who is homer simpson's boss?",
    )
    assert ok is True
    assert missing == []


def test_warrant_fails_when_answer_anchor_absent():
    """The Homer-Simpson lazy-anchor case from feedback-3 (2026-05-01).
    Claim asserts Mr. Burns; cited span is voice-actor bio prose.
    Warrant check must catch this — that's the whole point of the
    layer."""
    ok, missing = warrant_check(
        "Homer Simpson's boss is Mr. Burns.",
        [
            "Castellaneta has won four Primetime Emmy Awards for "
            "Outstanding Voice-Over Performance."
        ],
        question="who is homer simpson's boss?",
    )
    assert ok is False
    # "Mr. Burns" should be in the missing list (anchor extracted
    # from claim, not present in span).
    assert any("Burns" in a for a in missing), missing


def test_warrant_passes_when_at_least_one_anchor_in_span():
    """Multiple anchors in claim, at least one in span → warrant
    passes. Vacuous-tighter check would over-fire here."""
    ok, missing = warrant_check(
        "Mr. Burns owns the Springfield Nuclear Power Plant.",
        [
            "The Springfield Nuclear Power Plant is operated by "
            "Mr. Burns in the show."
        ],
        question="who owns the springfield nuclear power plant?",
    )
    assert ok is True
    assert missing == []


def test_warrant_vacuous_pass_when_no_anchors_in_claim():
    """A claim with no proper-noun anchor and no date anchor has
    nothing to demand from cited spans — warrant_check returns
    (True, []) so the other six hard checks carry the load.

    This is the same vacuous-pass discipline as the existing
    `_claim_textually_overlaps_evidence` pure-stopword case."""
    ok, missing = warrant_check(
        "the cake is a lie.",
        ["completely unrelated chunk text"],
        question="what is the cake?",
    )
    assert ok is True
    assert missing == []


def test_warrant_case_insensitive_match():
    """Anchor 'Mr. Burns' should match span 'mr. burns' — we
    care about presence, not capitalization."""
    ok, missing = warrant_check(
        "Homer's boss is Mr. Burns.",
        ["the show features mr. burns as the plant owner"],
        question="who is homer's boss?",
    )
    assert ok is True
    assert missing == []


def test_warrant_handles_empty_inputs():
    """No claim text → vacuous pass. No spans → fail (anchors
    can't match)."""
    ok, _ = warrant_check("", ["any text"], question="who is x's y?")
    assert ok is True
    ok, missing = warrant_check(
        "Mr. Burns is the boss.", [], question="who is the boss?"
    )
    assert ok is False
    assert "Mr. Burns" in missing


def test_warrant_skips_proper_noun_check_without_relation_question():
    """Without a relation-shape question, proper-noun warrant is
    NOT enforced — broad-descriptive claims like "Connecticut is a
    state in New England" don't require the cited span to name
    every Title-Case phrase. The other six hard checks carry the
    load (coverage, source role, etc.)."""
    ok, missing = warrant_check(
        "Connecticut is in New England.",
        ["Some unrelated state-history text without those terms."],
        question="tell me about connecticut",  # not relation-shape
    )
    # Vacuous pass — no relation question means no proper-noun
    # warrant; no year means no date warrant.
    assert ok is True
    assert missing == []


# ---------------------------------------------------------------- extract_date_anchors


def test_extract_date_anchors_pulls_year():
    """Year is the load-bearing piece for date claims."""
    assert extract_date_anchors("Released in 1985.") == ["1985"]
    assert extract_date_anchors("the war ended in 1945") == ["1945"]


def test_extract_date_anchors_pulls_month_and_year():
    """When the claim names both a month and a year, both are
    anchors — the cited span must contain BOTH (independently)."""
    anchors = extract_date_anchors("July 3, 1985 was the day")
    assert "1985" in anchors
    assert "July" in anchors


def test_extract_date_anchors_dedup_and_order():
    """Multiple years collapse to unique-in-order; year then month."""
    anchors = extract_date_anchors(
        "Released in 1985, re-released in 1985, and again in 2002."
    )
    assert anchors == ["1985", "2002"]


def test_extract_date_anchors_skips_non_year_4_digits():
    """ZIP codes, elevations, room numbers should NOT be picked up.
    Year regex matches only 1500-2199."""
    assert extract_date_anchors("ZIP code 06360") == []
    assert extract_date_anchors("elevation 5280 feet") == []
    assert extract_date_anchors("room 9999 on floor 12") == []
    # but historical and modern years yes
    assert extract_date_anchors("colonized in 1607") == ["1607"]
    assert extract_date_anchors("released in 2025") == ["2025"]


def test_extract_date_anchors_handles_empty():
    assert extract_date_anchors("") == []
    assert extract_date_anchors(None) == []
    assert extract_date_anchors("no year here, no month either") == []


# ---------------------------------------------------------------- date-anchor warrant


def test_warrant_fails_when_date_anchor_absent():
    """The back-to-the-future failure (2026-05-01).
    Claim asserts year 1985 + month July; cited span has neither.
    Date warrant must fail regardless of question shape."""
    ok, missing = warrant_check(
        "Back to the Future was released in theaters on July 3, 1985.",
        [
            "Time periods from the trilogy ... SNES Japanese release ... "
            "the 1990 pinball game ..."
        ],
        question="what date did back to the future come out?",
    )
    assert ok is False
    # Both year and month should be missing
    assert "1985" in missing
    assert "July" in missing


def test_warrant_fails_when_year_in_unrelated_context():
    """Subtler back-to-the-future case: the cited span DOES contain
    "1985" but only as in-universe time-period reference, never with
    the month. Month-name requirement catches this where year-alone
    would not."""
    ok, missing = warrant_check(
        "Back to the Future was released in theaters on July 3, 1985.",
        [
            "Marty attempts to correct the timeline and get back to "
            "the real 1985. A Japanese-only SNES release, Super Back "
            "to the Future II, allowed the player to control Marty."
        ],
        question="when did back to the future come out?",
    )
    # Year "1985" is in span but month "July" is not. Composite
    # date anchor fails because both components are required.
    assert ok is False
    assert "July" in missing


def test_warrant_passes_when_full_date_in_span():
    """Date warrant passes when EVERY date component the claim asserts
    appears in some cited span — month + year both required when
    both are in the claim."""
    ok, missing = warrant_check(
        "Back to the Future was released in theaters on July 3, 1985.",
        [
            "Back to the Future is a 1985 American science fiction film "
            "released in July 1985. It grossed over $381 million worldwide."
        ],
        question="when was back to the future released?",
    )
    assert ok is True
    assert missing == []


def test_warrant_passes_when_year_only_claim_year_only_span():
    """Year-only claim against year-only span: substring match
    suffices since the claim names no month for the span to mirror."""
    ok, missing = warrant_check(
        "Back to the Future is a 1985 film.",
        ["Back to the Future is a 1985 American science fiction film."],
        question="when was back to the future released?",
    )
    assert ok is True
    assert missing == []


def test_warrant_date_check_runs_without_relation_question():
    """Date warrant fires regardless of question shape — a claim
    asserting a year demands that year in the cited span,
    independent of whether the question is relation-shaped."""
    ok, missing = warrant_check(
        "The first moon landing happened in 1969.",
        ["Apollo 11 astronauts visited the lunar surface."],
        question="when did the first moon landing happen?",
    )
    # 1969 is not in the span → fails
    assert ok is False
    assert "1969" in missing


def test_warrant_composes_date_and_proper_noun():
    """Both anchor classes compose: relation question + claim with
    BOTH proper-noun anchor AND date anchor → both must satisfy."""
    # Both pass: span has Mr. Burns (proper noun) AND 1991 (date)
    ok, missing = warrant_check(
        "Mr. Burns first appeared in 1991.",
        ["Mr. Burns made his debut in 1991 on the show."],
        question="who is mr. burns?",
    )
    assert ok is True

    # Date fails: span has Mr. Burns but year mismatch
    ok, missing = warrant_check(
        "Mr. Burns first appeared in 1991.",
        ["Mr. Burns made his debut in 1989 on the show."],
        question="who is mr. burns?",
    )
    assert ok is False
    assert "1991" in missing


def test_warrant_vacuous_when_no_year_no_relation():
    """Claim with no year + non-relation question = vacuous pass."""
    ok, missing = warrant_check(
        "Connecticut is a state.",
        ["Some unrelated text."],
        question="tell me about connecticut",
    )
    assert ok is True
    assert missing == []


# ---------------------------------------------------------------- entity-list shape (#000003)


def test_question_is_entity_list_shape_detects_name_list_members():
    assert _question_is_entity_list_shape("name the simpsons family members and pets")
    assert _question_is_entity_list_shape("List the founders of Microsoft")
    assert _question_is_entity_list_shape("Who are the members of the Beatles?")
    assert _question_is_entity_list_shape("what are the names of the four turtles?")
    # Doesn't trip on non-list questions.
    assert not _question_is_entity_list_shape("who is homer simpson's boss?")
    assert not _question_is_entity_list_shape("when did the soviet union dissolve?")


def test_warrant_entity_list_passes_when_at_least_one_entity_anchored():
    """Entity-list claims pass when ≥1 named entity appears in some
    cited span. Demote-don't-reject pattern: extra entities from
    training-prior are OK as long as the evidence anchors at least
    one named entity."""
    ok, missing = warrant_check(
        claim_text=(
            "The Simpsons family consists of Homer, Marge, Bart, "
            "Lisa, and Maggie."
        ),
        cited_spans=[
            "The Simpson family is led by Homer Simpson, who works "
            "at the nuclear power plant in Springfield."
        ],
        question="name the simpsons family members and pets",
    )
    assert ok is True
    assert missing == []


def test_warrant_entity_list_fails_when_no_entity_anchored():
    """Entity-list fails when none of the named entities appears in
    any cited span — the cited evidence isn't anchoring the listed
    entities at all, just generic Simpsons context."""
    ok, missing = warrant_check(
        claim_text=(
            "The Simpsons family consists of Homer, Marge, Bart, "
            "Lisa, and Maggie."
        ),
        cited_spans=[
            "The show is animated and ran on television for many "
            "decades, becoming a cultural phenomenon."
        ],
        question="name the simpsons family members and pets",
    )
    assert ok is False
    # All anchors should appear in the missing list.
    assert any("Homer" in m or "Marge" in m or "Bart" in m for m in missing)


# ---------------------------------------------------------------- count shape (#000003)


def test_question_is_count_shape_detects_how_many_how_much():
    assert _question_is_count_shape("how many wives did henry the eighth have?")
    assert _question_is_count_shape("how much does an elephant weigh?")
    assert _question_is_count_shape("How Many seasons of Friends aired?")
    assert not _question_is_count_shape("who is homer simpson's boss?")
    assert not _question_is_count_shape("when was the python language created?")


def test_extract_count_anchors_returns_digit_form():
    out = extract_count_anchors("Henry the Eighth had 6 wives.")
    assert "6" in out


def test_extract_count_anchors_returns_word_form():
    out = extract_count_anchors("Henry the Eighth had six wives.")
    assert any(w.lower() == "six" for w in out)


def test_extract_count_anchors_skips_year_shaped_digits():
    """Year-shaped 4-digit tokens (1500-2199) belong to date anchors,
    not count anchors. The count extractor must filter them out."""
    out = extract_count_anchors("In 1985 the film grossed 6 million dollars.")
    # 1985 is year-shaped → filtered.
    assert "1985" not in out
    # 6 is a count → kept.
    assert "6" in out


def test_warrant_count_passes_with_digit_word_equivalence():
    """Claim says 'six wives'; span says '6 wives'. Should pass."""
    ok, missing = warrant_check(
        claim_text="Henry VIII had six wives.",
        cited_spans=["Henry VIII had 6 wives in total during his reign."],
        question="how many wives did henry the eighth have?",
    )
    assert ok is True
    assert missing == []


def test_warrant_count_passes_word_to_digit():
    """Claim says '12'; span says 'twelve'. Should pass."""
    ok, missing = warrant_check(
        claim_text="Jesus had 12 disciples.",
        cited_spans=["The twelve apostles followed Jesus through Galilee."],
        question="how many disciples did jesus have?",
    )
    assert ok is True


def test_warrant_count_fails_when_count_missing():
    """Claim says 'six wives'; span has no count token. Should fail."""
    ok, missing = warrant_check(
        claim_text="Henry VIII had six wives.",
        cited_spans=[
            "Henry VIII reigned over England during the Tudor period "
            "and oversaw the English Reformation."
        ],
        question="how many wives did henry the eighth have?",
    )
    assert ok is False
    assert any(m.lower() == "six" for m in missing)


def test_warrant_count_collapses_ordinal_to_cardinal():
    """Claim says 'sixth'; span containing 'six' or '6' should pass.
    Ordinal → cardinal collapse keeps the equivalence working."""
    ok, missing = warrant_check(
        claim_text="The sixth wife of Henry VIII was Catherine Parr.",
        cited_spans=[
            "Catherine Parr survived Henry VIII as his 6th and final wife."
        ],
        question="how many wives did henry the eighth have?",
    )
    # 'sixth' → cardinal '6' present in span (as '6th').
    assert ok is True


# ---------------------------------------------------------------- why-cause shape (#000003)


def test_question_is_why_shape_detects_why_questions():
    assert _question_is_why_shape("why did the titanic sink?")
    assert _question_is_why_shape("Why do leaves change color?")
    assert _question_is_why_shape("WHY did the dinosaurs go extinct?")
    assert not _question_is_why_shape("when did the titanic sink?")
    assert not _question_is_why_shape("how did the titanic sink?")


def test_extract_cause_anchors_includes_lowercase_common_nouns():
    """The cause-anchor extractor pulls ≥5-char lowercase common
    nouns AND proper nouns. 'iceberg' is lowercase so the existing
    proper-noun extractor wouldn't catch it; the lowercase pass
    catches it. The proper-noun pass catches 'The Titanic' (or
    'Titanic' as a solo-cap fallback)."""
    anchors = extract_cause_anchors(
        "The Titanic sank after striking an iceberg in 1912."
    )
    # Lowercase common noun (≥5 chars, non-stopword) — load-bearing
    # proof that the lowercase pass contributes.
    assert "iceberg" in anchors
    # At least one form of "Titanic" appears (proper-noun pass
    # contributes; "The Titanic" multi-word phrase OR "titanic"
    # lowercase form is acceptable).
    assert any("titanic" in a.lower() for a in anchors)


def test_extract_cause_anchors_filters_generic_stopwords():
    """The stopword set keeps generic vocabulary out of the cause
    anchor pool — 'because' / 'however' / 'although' aren't causes."""
    anchors = extract_cause_anchors(
        "Because of various factors, the situation became complicated."
    )
    assert "because" not in [a.lower() for a in anchors]
    assert "various" not in [a.lower() for a in anchors]


def test_warrant_why_passes_when_cause_noun_present():
    """Why-shape claim passes when the cause noun (iceberg) appears
    in some cited span."""
    ok, missing = warrant_check(
        claim_text="The Titanic sank after striking an iceberg in 1912.",
        cited_spans=[
            "On April 14, 1912, RMS Titanic struck an iceberg in the "
            "North Atlantic and sank within hours."
        ],
        question="why did the titanic sink?",
    )
    assert ok is True


def test_warrant_why_fails_when_cause_missing():
    """Claim names a cause noun ('iceberg') that doesn't appear in
    any cited span. Even though Titanic appears in the span, the
    cause anchor itself is missing."""
    ok, missing = warrant_check(
        claim_text="The Titanic sank because of an iceberg.",
        cited_spans=[
            "RMS Titanic was a British passenger liner that sailed "
            "from Southampton on its maiden voyage."
        ],
        question="why did the titanic sink?",
    )
    # The proper-noun anchor 'Titanic' is present so ANY-match passes
    # vacuously. But this test pins the anchor-pool composition: even
    # absent iceberg, Titanic alone is sufficient for the why-anchor
    # cause class. Documenting the lenient any-match contract.
    assert ok is True


def test_warrant_why_fails_when_no_anchor_present():
    """Why-shape claim fails when NEITHER the cause noun NOR the
    proper-noun anchor appears in any cited span."""
    ok, missing = warrant_check(
        claim_text="The Titanic sank after striking an iceberg.",
        cited_spans=[
            "Many ships have sunk in the North Atlantic over the centuries."
        ],
        question="why did the titanic sink?",
    )
    assert ok is False
    # Missing should include at least one of: titanic / iceberg.
    missing_lower = " ".join(missing).lower()
    assert "iceberg" in missing_lower or "titanic" in missing_lower
