"""ADR-0061 — PROCEDURE intent routes to pack-grounded surface.

Pre-ADR-0061, ``PROCEDURE`` intent had no pack-grounded composer:
every ``"How do I X?"`` question fell through to the universal
disclosure even when ``X`` was a pack-resident lemma.  This closed
``procedure_define_010`` ("How do I define a concept?") as a
surface-and-term holdout miss and ``procedure_verify_034``
("How do I verify a claim?") as a surface miss.

ADR-0061 adds ``pack_grounded_procedure_surface(subject_text)``:
extracts the **last** pack-resident lemma from the verb-phrase
subject (deliberately: procedure verb → topic, last is the topic)
and emits a deterministic acknowledgement surface that grounds
the topic in pack semantic_domains and notes explicitly that
step-by-step guidance is not yet ratified.

These tests pin:

  - Extraction picks the last pack-resident lemma (not the first).
  - Stopwords ``be`` / ``have`` are skipped.
  - Verbs (``define``, ``verify``, ``correct``, ``learn``) are NOT
    stopworded — when the verb is the only pack-resident lemma,
    the verb is the topic by elimination.
  - Surface is deterministic.
  - Surface preserves the trust-boundary clause about
    not-yet-ratified guidance.
  - Returns ``None`` when no pack lemma is found (so the universal
    disclosure still fires for fully-unknown procedure utterances).
  - Live ``ChatRuntime`` routes ``procedure_define_010`` through
    this composer and the surface contains ``concept``.
"""

from __future__ import annotations

from chat.pack_grounding import (
    PACK_ID,
    _extract_procedure_topic_lemma,
    pack_grounded_procedure_surface,
)
from chat.runtime import ChatRuntime


# ---------------------------------------------------------------------------
# Topic-lemma extraction (last-wins on procedure subjects)
# ---------------------------------------------------------------------------


def test_extract_picks_last_pack_lemma() -> None:
    """For verb-phrase subjects the topic is the object — last
    pack-resident lemma wins."""
    assert _extract_procedure_topic_lemma("define a concept") == "concept"


def test_extract_returns_verb_when_only_pack_lemma() -> None:
    """When the verb is the only pack-resident lemma (object is OOV
    or filler), the verb is the topic by elimination — preserves
    coverage on procedure utterances with non-pack objects."""
    # ``hypothesis`` is genuinely OOV across cognition/meta/relations.
    # Pre-en_core_meta_v1 the canonical example was "verify a claim",
    # but ``claim`` now resolves via the meta pack (meta.speech_act.claim),
    # so the object-first selector would correctly pick ``claim`` over
    # ``verify`` for that utterance.  Use ``hypothesis`` to keep the
    # verb-fallback contract isolated from pack-coverage changes.
    assert _extract_procedure_topic_lemma("verify a hypothesis") == "verify"
    assert _extract_procedure_topic_lemma("correct an error") == "correct"
    assert _extract_procedure_topic_lemma("learn this") == "learn"


def test_extract_skips_dialogue_fillers() -> None:
    """``be`` and ``have`` are pack-resident but stopworded."""
    assert _extract_procedure_topic_lemma("be a teacher") is None  # 'teacher' is OOV
    assert _extract_procedure_topic_lemma("have knowledge") == "knowledge"


def test_extract_none_when_no_pack_lemma() -> None:
    assert _extract_procedure_topic_lemma("") is None
    assert _extract_procedure_topic_lemma(None) is None  # type: ignore[arg-type]
    # Pre-en_core_action_v1 the canonical fixture was "do stuff", but
    # ``do`` now resolves via en_core_action_v1 (action.doing.perform).
    # Use a genuinely-OOV verb+noun pair to preserve the original
    # contract: when no pack-resident lemma exists, return None.
    assert _extract_procedure_topic_lemma("fix bugs") is None


def test_extract_strips_punctuation() -> None:
    assert _extract_procedure_topic_lemma("define, a concept.") == "concept"


def test_extract_is_case_insensitive() -> None:
    assert _extract_procedure_topic_lemma("DEFINE A CONCEPT") == "concept"


# ---------------------------------------------------------------------------
# Surface composition
# ---------------------------------------------------------------------------


def test_surface_contains_topic_lemma() -> None:
    surface = pack_grounded_procedure_surface("define a concept")
    assert surface is not None
    assert "concept" in surface


def test_surface_contains_topic_domains() -> None:
    """Pack-grounded: the topic lemma's top semantic_domains are
    surfaced verbatim — no rewording."""
    from chat.pack_grounding import _pack_index
    concept_domains = _pack_index().get("concept", ())
    assert concept_domains, "test fixture: 'concept' must be a pack lemma"

    surface = pack_grounded_procedure_surface("define a concept")
    assert surface is not None
    assert any(d in surface for d in concept_domains[:2])


def test_surface_contains_pack_id() -> None:
    surface = pack_grounded_procedure_surface("define a concept")
    assert surface is not None
    assert PACK_ID in surface


def test_surface_preserves_not_yet_ratified_clause() -> None:
    """Trust-boundary label: procedure guidance is not yet ratified.
    Must appear in every surface emitted by this composer."""
    surface = pack_grounded_procedure_surface("define a concept")
    assert surface is not None
    assert "not yet ratified" in surface


def test_surface_returns_none_for_no_pack_lemma() -> None:
    """A procedure subject with no pack-resident lemma falls
    through to the universal disclosure — preserves the honesty
    contract for fully-unknown procedures."""
    assert pack_grounded_procedure_surface("") is None
    # ``do`` is now pack-resident in en_core_action_v1; use a verb+noun
    # pair where both terms are genuinely OOV across all mounted packs.
    assert pack_grounded_procedure_surface("fix bugs") is None


def test_surface_is_deterministic() -> None:
    a = pack_grounded_procedure_surface("define a concept")
    b = pack_grounded_procedure_surface("define a concept")
    assert a == b


# ---------------------------------------------------------------------------
# End-to-end through ChatRuntime
# ---------------------------------------------------------------------------


def test_procedure_define_010_now_emits_concept() -> None:
    """The exact holdout case this ADR targets:
    `procedure_define_010` ("How do I define a concept?") expected
    ``term=['concept']`` and was missing it pre-ADR-0061.  Through
    the live runtime, the surface must now contain ``concept``."""
    rt = ChatRuntime()
    response = rt.chat("How do I define a concept?")
    assert response.grounding_source == "pack"
    assert "concept" in response.surface.lower()


def test_procedure_with_no_pack_lemma_routes_to_oov_invitation() -> None:
    """ADR-0065 / P2.1 — a procedure utterance with no pack-resident
    lemma now routes to the OOV invitation surface (names the unknown
    topic, points at PackMutationProposal path) instead of the flat
    universal disclosure.  No surface fabrication: the invitation
    only references the OOV token and the mounted-pack list."""
    rt = ChatRuntime()
    # ``fix`` and ``bugs`` are genuinely OOV across all mounted packs
    # (``do`` is now pack-resident in en_core_action_v1, so the prior
    # fixture "how do I do stuff?" would extract ``do`` and ground).
    response = rt.chat("How do I fix bugs?")
    # Either UNKNOWN-intent → "none", or PROCEDURE-intent on OOV
    # subject → "oov" invitation.  Both honour the no-fabrication
    # contract; the surface text differs by intent classification.
    assert response.grounding_source in {"oov", "none"}
    if response.grounding_source == "oov":
        assert "PackMutationProposal" in response.surface


def test_procedure_verify_an_oov_object_falls_back_to_verb() -> None:
    """When the object is OOV but the verb is pack-resident, the
    procedure composer surfaces the verb — keeps surface_groundedness
    coverage on utterances whose object is not yet packed.

    ``hypothesis`` is genuinely OOV across cognition/meta/relations.
    """
    rt = ChatRuntime()
    response = rt.chat("How do I verify a hypothesis?")
    assert response.grounding_source == "pack"
    assert "verify" in response.surface.lower()
