"""Intent classification extensions — close five concrete failures
surfaced by the cumulative live probe (2026-05-19).

Before these changes, the runtime returned ``grounding_source="none"``
for ~50% of realistic conversational definition prompts even though
every subject lemma was pack-resident.  The bottleneck was intent
classification + subject extraction, not lexicon coverage.

Five gaps pinned by this file:

  1. ``Define X``        — imperative DEFINITION had no rule;
                           prompt fell through to UNKNOWN.
  2. ``What does X mean?`` — matched TRANSITIVE_QUERY for which the
                            runtime has no pack-grounded handler.
                            Now re-routes to DEFINITION when the
                            transitive relation is ``mean``/``means``.
  3. ``What is to V?``   — DEFINITION subject was ``to V`` (un-stripped
                           infinitive marker), so pack resolution
                           failed.  Now strips ``to`` for DEFINITION /
                           RECALL.
  4. ``How does X work?`` — no rule; only first-person PROCEDURE
                            ("How do I X?") was wired.  Now matches a
                            dedicated mechanistic-cause regex and
                            routes to CAUSE.
  5. ``What causes X?``   — no rule; the causative-verb family
                            (causes/triggers/enables/prevents/drives/
                            produces/induces/yields) now routes to
                            CAUSE with X as subject.

A sixth runtime-side decision was *considered* (CAUSE / VERIFICATION
falling through to ``pack_grounded_surface`` when no teaching chain
exists) but deliberately not adopted.  Doing so would mask the
teaching-gap signal the discovery layer uses to identify chains worth
authoring — see ``tests/test_discovery_candidates``.  The
``TestCauseVerificationNoPackFallback`` class below pins this doctrine.
"""

from __future__ import annotations

from chat.runtime import ChatRuntime
from generate.intent import IntentTag, classify_intent


class TestDefineRule:
    def test_define_routes_to_definition(self) -> None:
        intent = classify_intent("Define moment.")
        assert intent.tag is IntentTag.DEFINITION
        assert intent.subject == "moment"

    def test_define_strips_trailing_punctuation(self) -> None:
        for text in ("Define moment.", "Define moment", "Define moment!"):
            intent = classify_intent(text)
            assert intent.tag is IntentTag.DEFINITION
            assert intent.subject == "moment"

    def test_define_multi_word_subject_preserved(self) -> None:
        intent = classify_intent("Define artificial intelligence")
        assert intent.tag is IntentTag.DEFINITION
        assert intent.subject == "artificial intelligence"

    def test_define_grounds_on_pack_lemma(self) -> None:
        response = ChatRuntime().chat("Define moment.")
        assert response.grounding_source == "pack"
        assert "moment" in response.surface.lower()
        assert "temporal" in response.surface.lower()


class TestWhatDoesXMean:
    def test_routes_to_definition_not_transitive_query(self) -> None:
        intent = classify_intent("What does important mean?")
        assert intent.tag is IntentTag.DEFINITION
        assert intent.subject == "important"

    def test_means_form_also_routes(self) -> None:
        intent = classify_intent("What does X means?")
        assert intent.tag is IntentTag.DEFINITION

    def test_other_transitive_relations_preserve_tag(self) -> None:
        """Only ``mean``/``means`` re-route; other relations remain
        TRANSITIVE_QUERY so multi_relation_walk still fires."""
        intent = classify_intent("What does wisdom precede?")
        assert intent.tag is IntentTag.TRANSITIVE_QUERY
        assert intent.relation == "precedes"

    def test_grounds_on_pack_lemma(self) -> None:
        response = ChatRuntime().chat("What does soon mean?")
        assert response.grounding_source == "pack"
        assert "temporal" in response.surface.lower()


class TestInfinitiveMarkerStripped:
    def test_what_is_to_create_strips_to(self) -> None:
        intent = classify_intent("What is to create?")
        assert intent.tag is IntentTag.DEFINITION
        assert intent.subject == "create"

    def test_what_is_to_remember_strips_to(self) -> None:
        intent = classify_intent("What is to remember?")
        assert intent.tag is IntentTag.DEFINITION
        assert intent.subject == "remember"

    def test_grounds_on_packlemma_after_strip(self) -> None:
        response = ChatRuntime().chat("What is to create?")
        assert response.grounding_source == "pack"

    def test_to_not_stripped_from_verification_subject(self) -> None:
        """``to`` as a preposition (not infinitive) must NOT be
        stripped from VERIFICATION subjects.  The aux-verb strip in
        VERIFICATION only takes the head noun anyway, so this is
        defensive — the infinitive strip is gated on intent tag."""
        intent = classify_intent("Is X bound to Y?")
        # VERIFICATION strips aux verbs and articles, returns head noun.
        # The infinitive strip is DEFINITION/RECALL only.
        assert intent.tag is IntentTag.VERIFICATION


class TestHowDoesXWork:
    def test_third_person_mechanistic_query_routes_to_cause(self) -> None:
        intent = classify_intent("How does memory work?")
        assert intent.tag is IntentTag.CAUSE
        assert intent.subject == "memory"

    def test_all_mechanistic_verbs_route_to_cause(self) -> None:
        for verb in ("work", "function", "operate", "happen", "exist", "behave"):
            intent = classify_intent(f"How does memory {verb}?")
            assert intent.tag is IntentTag.CAUSE, f"verb={verb}"
            assert intent.subject == "memory"

    def test_first_person_procedure_still_wins(self) -> None:
        """The PROCEDURE rule must still fire for first-person form."""
        intent = classify_intent("How do I verify a hypothesis?")
        assert intent.tag is IntentTag.PROCEDURE

    def test_routes_to_cause_but_returns_none_when_no_teaching_chain(self) -> None:
        """CAUSE on a pack-resident lemma with no teaching chain DOES
        NOT silently fall through to a pack disclosure — that would
        mask the teaching-gap signal the discovery layer uses to
        identify chains to add.  The grounding stays ``none`` so the
        teaching pipeline records a learning opportunity.
        """
        response = ChatRuntime().chat("How does memory work?")
        # CAUSE intent fired, lemma is pack-resident, but no teaching
        # chain exists → grounding_source is "none", which is the
        # honest no-answer signal (not a fabricated cause).
        assert response.grounding_source == "none"


class TestWhatCausesX:
    def test_what_causes_routes_to_cause(self) -> None:
        intent = classify_intent("What causes doubt?")
        assert intent.tag is IntentTag.CAUSE
        assert intent.subject == "doubt"

    def test_all_causative_verbs_route_to_cause(self) -> None:
        for verb in (
            "causes", "triggers", "enables", "prevents",
            "drives", "produces", "induces", "yields",
        ):
            intent = classify_intent(f"What {verb} understanding?")
            assert intent.tag is IntentTag.CAUSE, f"verb={verb}"
            assert intent.subject == "understanding"

    def test_what_is_unchanged(self) -> None:
        """Generic ``What is X?`` must still match DEFINITION first."""
        intent = classify_intent("What is doubt?")
        assert intent.tag is IntentTag.DEFINITION

    def test_returns_none_when_no_causal_teaching_chain(self) -> None:
        """Same honesty contract as the mechanistic-cause path: when
        no teaching chain answers the cause, ``grounding_source`` is
        ``none`` so the teaching pipeline can log the gap."""
        response = ChatRuntime().chat("What causes doubt?")
        assert response.grounding_source == "none"


class TestCauseVerificationNoPackFallback:
    """The runtime dispatcher deliberately does NOT fall through to
    ``pack_grounded_surface`` for CAUSE / VERIFICATION when no
    teaching chain matches.  Doing so would mask the discovery layer's
    teaching-gap signal — see ``tests/test_discovery_candidates``.
    """

    def test_oov_subject_returns_oov(self) -> None:
        response = ChatRuntime().chat("Why does triangle exist?")
        assert response.grounding_source in {"oov", "none"}

    def test_pack_lemma_without_chain_returns_none(self) -> None:
        """``doubt`` is pack-resident but has no CAUSE-rooted teaching
        chain — grounding_source must be ``none`` so the discovery
        candidate is emitted to flag the teaching gap."""
        response = ChatRuntime().chat("Is doubt evident?")
        assert response.grounding_source == "none"


class TestConfirmationTagNormalization:
    def test_trailing_confirmation_tag_is_stripped_from_relation_claim(self) -> None:
        intent = classify_intent("Light reveals truth, right?")
        assert intent.tag is IntentTag.VERIFICATION
        assert intent.subject == "light"
        assert intent.relation == "reveals"
        assert intent.object == "truth"
        assert intent.secondary_subject == "truth"
        assert intent.negated is False

    def test_ok_tag_after_period_is_stripped(self) -> None:
        intent = classify_intent("Knowledge supports truth. ok?")
        assert intent.tag is IntentTag.VERIFICATION
        assert intent.subject == "knowledge"
        assert intent.relation == "supports"
        assert intent.object == "truth"

    def test_content_word_right_is_not_stripped(self) -> None:
        intent = classify_intent("Is right an axis?")
        assert intent.tag is IntentTag.VERIFICATION
        assert intent.subject == "right"

    def test_bare_leading_particle_is_not_erased(self) -> None:
        intent = classify_intent("yes?")
        assert intent.tag is IntentTag.UNKNOWN
        assert intent.subject == "yes?"

    def test_negative_relation_claim_survives_tag_strip(self) -> None:
        intent = classify_intent("Light does not reveal truth, right?")
        assert intent.tag is IntentTag.VERIFICATION
        assert intent.subject == "light"
        assert intent.relation == "reveals"
        assert intent.object == "truth"
        assert intent.negated is True

    def test_runtime_relation_confirmation_gets_pack_surface(self) -> None:
        response = ChatRuntime().chat("Light reveals truth, right?")
        assert response.grounding_source == "pack"
        assert response.realizer_guard_status == "ok"
        assert response.realizer_guard_rule == ""
        assert "light reveals truth" in response.surface

    def test_runtime_negative_relation_confirmation_passes_guard(self) -> None:
        response = ChatRuntime().chat("Light does not reveal truth, right?")
        assert response.grounding_source == "pack"
        assert response.realizer_guard_status == "ok"
        assert "light does not reveal truth" in response.surface


class TestCumulativeLiftInvariant:
    """Pins the lift observed by the 2026-05-19 cumulative live probe:
    the DEFINITION-shaped prompts must produce ``pack`` grounding,
    since every gap surfaced for them was an intent-classification
    fix that has now landed.

    CAUSE-shaped prompts (``How does X work?``, ``What causes X?``)
    deliberately route to ``none`` when no teaching chain exists —
    this is the honest signal that drives the teaching pipeline.
    """

    DEFINITION_SAMPLE = (
        "Define moment.",
        "What does important mean?",
        "What is to create?",
    )

    def test_definition_sample_all_pack_grounded(self) -> None:
        for prompt in self.DEFINITION_SAMPLE:
            r = ChatRuntime().chat(prompt)
            assert r.grounding_source == "pack", (
                f"prompt {prompt!r} regressed: grounding_source={r.grounding_source}"
            )
