"""Tests for the Phase-1 question-frame reader lifecycle (ADR-0164.3)."""

from __future__ import annotations

import pytest

from generate.comprehension.lifecycle import (
    apply_word,
    begin_sentence,
    end_sentence,
)
from generate.comprehension.state import (
    EntityRef,
    ProblemReadingState,
    ReaderRefusal,
    SentenceReadingState,
)


def _empty_problem(
    *,
    registry: tuple[EntityRef, ...] = (),
    sentence_index: int = 0,
) -> ProblemReadingState:
    return ProblemReadingState(
        entity_registry=registry,
        accumulated_initial_state=(),
        accumulated_operations=(),
        unknown_target_slot=None,
        pronoun_resolution_history=(),
        sentence_index=sentence_index,
        source_text_offset=0,
    )


def _read_sentence(
    words: list[str],
    problem_state: ProblemReadingState,
) -> SentenceReadingState | ReaderRefusal:
    """Drive a full sentence through apply_word. Returns final state or refusal."""
    state: SentenceReadingState | ReaderRefusal = begin_sentence(problem_state, 0)
    assert isinstance(state, SentenceReadingState)
    for word in words:
        result = apply_word(state, problem_state, word)
        if isinstance(result, ReaderRefusal):
            return result
        state = result
    return state


# ---------------------------------------------------------------------------
# Determinism
# ---------------------------------------------------------------------------


class TestDeterminism:
    def test_apply_word_byte_equal_outputs(self) -> None:
        ps = _empty_problem(registry=(EntityRef("monica", "female", 0),))
        s1 = begin_sentence(ps, 0)
        s2 = begin_sentence(ps, 0)
        r1 = apply_word(s1, ps, "How")
        r2 = apply_word(s2, ps, "How")
        assert isinstance(r1, SentenceReadingState)
        assert isinstance(r2, SentenceReadingState)
        assert r1.canonical_bytes() == r2.canonical_bytes()
        assert r1.canonical_hash() == r2.canonical_hash()

    def test_full_sentence_byte_equal(self) -> None:
        ps = _empty_problem(registry=(EntityRef("monica", "female", 0),))
        words = ["How", "much", "time", "did", "she", "spend", "?"]
        a = _read_sentence(words, ps)
        b = _read_sentence(words, ps)
        assert isinstance(a, SentenceReadingState)
        assert isinstance(b, SentenceReadingState)
        assert a.canonical_bytes() == b.canonical_bytes()


# ---------------------------------------------------------------------------
# Five GSM8K target question sentences
# ---------------------------------------------------------------------------


class TestGSM8KQuestions:
    def test_0007_how_many_more_boxes(self) -> None:
        ps = _empty_problem(
            registry=(EntityRef("francine_and_friend", "unknown", 0),)
        )
        words = [
            "How", "many", "more", "boxes", "do", "they", "need",
            "if", "Francine", "has", "a", "total", "of", "85", "crayons", "?",
        ]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState), state
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState), end
        target = end.unknown_target_slot
        assert target is not None
        assert target.entity == "francine_and_friend"
        assert target.unit_class == "count"
        assert target.kind == "difference"

    def test_0017_how_much_cost_him(self) -> None:
        ps = _empty_problem(
            registry=(
                EntityRef("eric", "male", 0),
                EntityRef("house", "neuter", 1),
            )
        )
        words = ["How", "much", "will", "it", "cost", "him", "?"]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState), state
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState), end
        target = end.unknown_target_slot
        assert target is not None
        assert target.entity == "eric"
        assert target.unit_class == "currency"
        assert target.kind == "continuous_quantity"

    def test_0027_how_many_followers_malcolm(self) -> None:
        ps = _empty_problem()
        words = [
            "How", "many", "followers", "does", "Malcolm", "have",
            "on", "all", "his", "social", "media", "?",
        ]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState), state
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState), end
        target = end.unknown_target_slot
        assert target is not None
        assert target.entity == "Malcolm"
        assert target.unit_class == "count"
        assert target.kind == "discrete_quantity"
        # Proper-noun entity entered the registry.
        names = [e.canonical_name for e in end.entity_registry]
        assert "Malcolm" in names

    def test_0036_how_much_time_studying(self) -> None:
        ps = _empty_problem(registry=(EntityRef("monica", "female", 0),))
        words = [
            "How", "much", "time", "did", "she", "spend", "studying",
            "in", "total", "during", "the", "five", "days", "?",
        ]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState), state
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState), end
        target = end.unknown_target_slot
        assert target is not None
        assert target.entity == "monica"
        assert target.unit_class == "time"
        assert target.kind == "continuous_quantity"

    def test_0043_how_much_money_left(self) -> None:
        ps = _empty_problem(registry=(EntityRef("sandra", "female", 0),))
        words = [
            "How", "much", "money", "will", "she", "be", "left",
            "with", "after", "the", "purchase", "?",
        ]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState), state
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState), end
        target = end.unknown_target_slot
        assert target is not None
        assert target.entity == "sandra"
        assert target.unit_class == "currency"
        assert target.kind == "continuous_quantity"


# ---------------------------------------------------------------------------
# Refusal modes
# ---------------------------------------------------------------------------


class TestRefusals:
    def test_unknown_word(self) -> None:
        ps = _empty_problem()
        s = begin_sentence(ps, 0)
        r = apply_word(s, ps, "@@@")
        assert isinstance(r, ReaderRefusal)
        assert r.reason == "unknown_word"
        assert r.token_text == "@@@"

    def test_statement_frame_opener_accepted(self) -> None:
        """Phase 2 (ADR-0164.4): proper noun at position 0 opens a statement
        pre-frame. After Brief 8.2's gender-enrichment refactor, the lookup
        skips proper_noun_gender_* categories and the proper_noun_token
        primitive admits "Francine" — Phase 2 then routes it to the
        statement-frame pre-frame entity slot instead of refusing.
        """
        ps = _empty_problem(registry=(EntityRef("francine", "female", 0),))
        s = begin_sentence(ps, 0)
        r = apply_word(s, ps, "Francine")
        assert isinstance(r, SentenceReadingState)
        assert r.frame is None  # frame determined on next verb
        assert r.pending_entity_ref is not None
        assert r.pending_entity_ref.canonical_name == "francine"

    def test_unresolved_pronoun_empty_registry(self) -> None:
        """A pronoun with no compatible entity refuses cleanly."""
        ps = _empty_problem()  # empty registry
        s = begin_sentence(ps, 0)
        s = apply_word(s, ps, "How")
        assert isinstance(s, SentenceReadingState)
        s = apply_word(s, ps, "much")
        assert isinstance(s, SentenceReadingState)
        s = apply_word(s, ps, "money")
        assert isinstance(s, SentenceReadingState)
        s = apply_word(s, ps, "will")
        assert isinstance(s, SentenceReadingState)
        r = apply_word(s, ps, "she")
        assert isinstance(r, ReaderRefusal)
        assert r.reason == "unresolved_pronoun"
        assert r.token_text == "she"

    def test_unfinished_frame_on_end(self) -> None:
        ps = _empty_problem()
        s = begin_sentence(ps, 0)
        # No words applied → frame is still None.
        end = end_sentence(s, ps)
        assert isinstance(end, ReaderRefusal)
        assert end.reason == "unfinished_frame"

    def test_unattached_quantity_on_end(self) -> None:
        """A SentenceReadingState with frame set but pending_quantities
        non-empty refuses with unattached_quantity at end_sentence."""
        from decimal import Decimal

        from generate.comprehension.state import QuantityRef

        ps = _empty_problem()
        # Construct a hand-built state to isolate the rule.
        pending = QuantityRef(
            value=Decimal("18"),
            unit=None,
            unit_class="pending",
            owner_entity=None,
            mention_position=0,
        )
        state = SentenceReadingState(
            entities=(),
            quantities=(),
            operations=(),
            frame="question_frame",
            pending_quantities=(pending,),
            token_index=2,
        )
        end = end_sentence(state, ps)
        assert isinstance(end, ReaderRefusal)
        assert end.reason == "unattached_quantity"

    def test_incomplete_operation_no_unit(self) -> None:
        """question_frame closes with no unit_class on the target → refuse."""
        ps = _empty_problem(registry=(EntityRef("monica", "female", 0),))
        s = begin_sentence(ps, 0)
        # Only "How" then "?" — no unit was ever set.
        s = apply_word(s, ps, "How")
        assert isinstance(s, SentenceReadingState)
        s = apply_word(s, ps, "?")
        assert isinstance(s, SentenceReadingState)
        end = end_sentence(s, ps)
        assert isinstance(end, ReaderRefusal)
        assert end.reason == "incomplete_operation"


# ---------------------------------------------------------------------------
# Lifecycle invariants
# ---------------------------------------------------------------------------


class TestLifecycleInvariants:
    def test_problem_state_preserved_when_sentence_introduces_no_entity(self) -> None:
        """begin → apply_word(*) → end_sentence preserves the registry
        when the sentence only references existing entities."""
        registry = (EntityRef("monica", "female", 0),)
        ps = _empty_problem(registry=registry)
        words = ["How", "much", "time", "did", "she", "spend", "?"]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState)
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState)
        assert end.entity_registry == registry

    def test_sentence_index_advances(self) -> None:
        ps = _empty_problem(registry=(EntityRef("monica", "female", 0),))
        words = ["How", "much", "time", "did", "she", "spend", "?"]
        state = _read_sentence(words, ps)
        assert isinstance(state, SentenceReadingState)
        end = end_sentence(state, ps)
        assert isinstance(end, ProblemReadingState)
        assert end.sentence_index == 1


class TestInitialDispatchAndUnknownGender:
    """ADR-0164.1 amendment (Brief 8.2): sentence-initial lookup-first +
    universal proper_noun_token primitive + unknown-gender pronoun
    resolution via single-salient fallback (ADR-0164.2)."""

    def test_sentence_initial_common_words_lookup_first(self) -> None:
        """Sentence-initial 'The'/'She'/'How' resolve via lexicon, not
        the proper_noun_token primitive. 'The' drains; 'She' refuses
        (pronoun outside question_frame); 'How' opens question_frame."""
        ps = _empty_problem()
        state = begin_sentence(ps, 0)
        out = apply_word(state, ps, "The")
        assert isinstance(out, SentenceReadingState)
        assert out.lookback[-1].category == "drain_token"

        state = begin_sentence(ps, 0)
        out = apply_word(state, ps, "She")
        assert isinstance(out, ReaderRefusal)
        # Phase 2: pronoun at position 0 attempts resolution; empty
        # registry → unresolved_pronoun per ADR-0164.2.
        assert out.reason == "unresolved_pronoun"

        state = begin_sentence(ps, 0)
        out = apply_word(state, ps, "How")
        assert isinstance(out, SentenceReadingState)
        assert out.frame == "question_frame"

    def test_sentence_initial_marnie_is_not_gated_by_gender_list(self) -> None:
        """'Marnie' is not in proper_noun_gender_female (after Brief 8.2
        rename dropped marnie from the female list). Reader admits her
        as a proper_noun_token at sentence-initial position. Under
        Phase 2, this opens a statement pre-frame (not a refusal).
        EntityRef carries gender="unknown" because the name is outside
        the curated gender lists.
        """
        ps = _empty_problem()
        out = apply_word(begin_sentence(ps, 0), ps, "Marnie")
        assert isinstance(out, SentenceReadingState)
        assert out.pending_entity_ref is not None
        assert out.pending_entity_ref.gender == "unknown"

    def test_sentence_initial_novel_name_uses_primitive_and_unknown_gender(self) -> None:
        """A name not in either gender list (e.g. 'Zelda') still admits
        via the universal proper_noun_token primitive. Under Phase 2,
        this opens a statement pre-frame with gender='unknown'."""
        ps = _empty_problem()
        state = begin_sentence(ps, 0)
        out = apply_word(state, ps, "Zelda")
        assert isinstance(out, SentenceReadingState)
        assert out.pending_entity_ref is not None
        assert out.pending_entity_ref.canonical_name == "zelda"
        assert out.pending_entity_ref.gender == "unknown"

    def test_pronoun_single_unknown_entity_resolves(self) -> None:
        """ADR-0164.2 single-salient fallback: one gender-unknown entity
        resolves the pronoun cleanly."""
        ps = _empty_problem(registry=(EntityRef("Zelda", "unknown", 0),))
        state = _read_sentence(
            ["How", "much", "money", "will", "she", "earn", "?"], ps
        )
        assert isinstance(state, SentenceReadingState), state
        assert state.question_target is not None
        assert state.question_target.entity == "Zelda"

    def test_pronoun_two_unknown_entities_refuses_ambiguous(self) -> None:
        """ADR-0164.2: two gender-unknown entities + no recency
        disambiguation → ambiguous_pronoun_referent."""
        ps = _empty_problem(
            registry=(
                EntityRef("Zelda", "unknown", 0),
                EntityRef("Marnie", "unknown", 1),
            )
        )
        state = begin_sentence(ps, 0)
        assert isinstance(state, SentenceReadingState)
        for token in ["How", "much", "money", "will"]:
            state = apply_word(state, ps, token)
            assert isinstance(state, SentenceReadingState)
        out = apply_word(state, ps, "she")
        assert isinstance(out, ReaderRefusal)
        assert out.reason == "ambiguous_pronoun_referent"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
