"""ADR-0049 — intent classifier subject extraction tests.

Contract pinned here:

  - Articles ("a", "an", "the") are stripped from the subject phrase
    for every intent that runs through the rule table.
  - For CAUSE and VERIFICATION intents, the subject is reduced to the
    head noun: leading auxiliary verbs ("does", "is", "can", ...) are
    stripped, then the first remaining token is returned.
  - For DEFINITION / RECALL / PROCEDURE intents, multi-word noun
    phrases are preserved (only articles + trailing punctuation are
    stripped) so that proper noun phrases like "artificial
    intelligence" survive.
  - Trailing punctuation (``?``, ``.``, ``!``) is removed.
  - Empty / all-stopword inputs fall back to the original cleaned
    phrase rather than producing an empty subject.
  - The normalizer is pack-agnostic: no pack loading, no pack-keyed
    lookup; this is a pure syntactic transform.

These tests are intentionally narrow and pin only the post-processor
behaviour.  Downstream tests (``test_pack_grounding``) cover the
end-to-end lift from this change reaching the pack-grounded surface.
"""

from __future__ import annotations

import pytest

from generate.intent import (
    DialogueIntent,
    IntentTag,
    classify_intent,
)


# ---------------------------------------------------------------------------
# DEFINITION — article stripping
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "prompt,expected_subject",
    [
        ("What is a procedure?", "procedure"),
        ("What is a relation?", "relation"),
        ("What is an answer?", "answer"),
        ("What is the truth?", "truth"),
        ("What is light?", "light"),  # already single-word, no change
        ("What is artificial intelligence?", "artificial intelligence"),  # multi-word noun phrase preserved
    ],
)
def test_definition_strips_articles(prompt: str, expected_subject: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.DEFINITION
    assert intent.subject == expected_subject


# ---------------------------------------------------------------------------
# CAUSE — head-noun extraction past leading aux verb
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "prompt,expected_subject",
    [
        ("Why does light exist?", "light"),
        ("Why does knowledge require evidence?", "knowledge"),
        ("Why is memory important?", "memory"),
        ("Why are categories useful?", "categories"),
        ("Why can a procedure fail?", "procedure"),  # aux 'can' then article 'a'
    ],
)
def test_cause_extracts_head_noun(prompt: str, expected_subject: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.CAUSE
    assert intent.subject == expected_subject


# ---------------------------------------------------------------------------
# VERIFICATION — head-noun extraction past leading aux verb
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "prompt,expected_subject",
    [
        ("Does memory require recall?", "memory"),
        ("Is light a wave?", "light"),
        ("Can a procedure fail?", "procedure"),
        ("Are categories useful?", "categories"),
        ("Has truth been defined?", "truth"),
    ],
)
def test_verification_extracts_head_noun(prompt: str, expected_subject: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.VERIFICATION
    assert intent.subject == expected_subject


# ---------------------------------------------------------------------------
# RECALL — already minimal, articles still stripped
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "prompt,expected_subject",
    [
        ("Remember light", "light"),
        ("Remember the truth", "truth"),
        ("Remember a procedure", "procedure"),
        # ``recall`` is a synonym imperative of ``remember`` and must
        # route identically.  The articulation breadth benchmark probe
        # ``"Recall truth."`` was misclassified as UNKNOWN until the
        # trigger pattern in ``_RULES`` was widened to ``(?:remember|
        # recall)\s+`` — without this case the regression silently
        # returns.
        ("Recall light", "light"),
        ("Recall the truth", "truth"),
        ("Recall a procedure", "procedure"),
        ("Recall truth.", "truth"),
    ],
)
def test_recall_strips_articles(prompt: str, expected_subject: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.RECALL
    assert intent.subject == expected_subject


# ---------------------------------------------------------------------------
# CORRECTION — word-boundary discipline on the trigger pattern
# ---------------------------------------------------------------------------
#
# Until a recent fix, the CORRECTION regex matched the bare token ``no``
# without word boundaries.  Combined with ``re.match``'s start anchor,
# every prompt beginning with ``No``-as-prefix (``Notice``, ``Note``,
# ``Now``, ``Nothing``, ``Nominate``, ``Norma``, ``Notwithstanding``)
# silently routed to CORRECTION with a mangled subject like
# ``"w remember light"`` (from ``"Now remember light."``).  The same
# hazard threatened ``incorrect`` / ``incorrectly``, ``actually`` /
# ``actualization``, ``correction`` / ``corrections``.  The fix added
# ``\b`` anchors on both sides of the alternation; these parametrized
# cases pin the boundary discipline against regression.


@pytest.mark.parametrize(
    "prompt",
    [
        # Pre-existing canonical forms (contracted ``that's``)
        "No, that's wrong.",
        "No.",
        "No way.",
        "no, knowledge is wrong.",
        "Incorrect.",
        "Actually, that's false.",
        "Correction: memory is not storage.",
        "That's wrong.",
        # Fully-spoken copula forms — added when the contracted-only
        # ``that'?s\s+(?:not|wrong)`` slot was widened to also accept
        # ``that\s+(?:is|was)`` and the predicate alternation grew
        # ``incorrect|false|mistaken``.  Every one of these used to
        # silently fall through to UNKNOWN.
        "That is not right.",
        "That is wrong.",
        "That was wrong.",
        "That is incorrect.",
        "That is false.",
        "That was not right.",
        "that is mistaken.",
        "That was incorrect.",
    ],
)
def test_correction_canonical_forms_still_route(prompt: str) -> None:
    """Legitimate CORRECTION pragmas must classify after the
    word-boundary fix narrowed the alternation, AND the
    fully-spoken copula variants must route too (previously UNKNOWN)."""
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.CORRECTION


@pytest.mark.parametrize(
    "prompt",
    [
        # ``No``-prefixed words that previously misfired
        "Nothing matters.",
        "Notice the truth.",
        "Note that recall fires.",
        "Nominate a candidate.",
        "Now remember light.",
        "Norma is here.",
        "Notwithstanding the evidence.",
        # ``Incorrect``-prefixed / ``Correction``-prefixed words
        "Incorrectly stated.",
        "Corrections department.",
        # ``Actually`` prefix — rarer but symmetric
        "Actualization of intent.",
        # Affirmatives that share the ``That is/was ...`` shape — the
        # predicate alternation (``not|wrong|incorrect|false|mistaken``)
        # must not over-match.  ``That is correct/right/true`` are NOT
        # corrections; ``falsifiable`` / ``wrongly accused`` carry the
        # trigger root but extend past the boundary.
        "That is correct.",
        "That is right.",
        "That is true.",
        "That works.",
        "That is interesting.",
        "That is a fact.",
        "That was a good question.",
        "That is falsifiable.",
        "That was wrongly accused.",
    ],
)
def test_correction_does_not_eat_no_prefixed_words(prompt: str) -> None:
    """Words beginning with the CORRECTION trigger letters must not
    silently route to CORRECTION via a missing word-boundary anchor."""
    intent = classify_intent(prompt)
    assert intent.tag is not IntentTag.CORRECTION


# ---------------------------------------------------------------------------
# Edge cases — degenerate inputs do not produce empty subjects
# ---------------------------------------------------------------------------


def test_definition_with_only_article_falls_back() -> None:
    """``What is the?`` is malformed; the normalizer must not empty the
    subject — it falls back to the cleaned original."""
    intent = classify_intent("What is the?")
    assert intent.tag is IntentTag.DEFINITION
    assert intent.subject != ""


def test_verification_with_only_aux_falls_back() -> None:
    """``Is is?`` is degenerate; the normalizer must not empty the subject."""
    # The rule table will match this as VERIFICATION; head-noun extraction
    # would strip all tokens, so the fallback path kicks in.
    intent = classify_intent("Is is is?")
    assert intent.tag is IntentTag.VERIFICATION
    assert intent.subject != ""


def test_empty_prompt_returns_unknown_with_empty_subject() -> None:
    intent = classify_intent("")
    assert intent.tag is IntentTag.UNKNOWN
    assert intent.subject == ""


def test_unknown_intent_preserves_raw_subject() -> None:
    """UNKNOWN-tag prompts bypass the normalizer entirely so the raw
    input survives for debugging / future-pattern detection."""
    intent = classify_intent("light logos")
    assert intent.tag is IntentTag.UNKNOWN
    assert intent.subject == "light logos"


# ---------------------------------------------------------------------------
# Trailing punctuation
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "prompt",
    [
        "What is light?",
        "What is light.",
        "What is light!",
        "What is light",
    ],
)
def test_trailing_punctuation_does_not_affect_subject(prompt: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.DEFINITION
    assert intent.subject == "light"


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


def test_normalization_is_deterministic() -> None:
    """Same prompt must produce byte-identical DialogueIntent on repeat
    classification — no randomness, no state."""
    prompt = "Why does memory require recall?"
    seen: set[DialogueIntent] = set()
    for _ in range(5):
        seen.add(classify_intent(prompt))
    assert len(seen) == 1


# ---------------------------------------------------------------------------
# Existing intent-test contract still holds (loose ``in subject.lower()``)
# ---------------------------------------------------------------------------


def test_legacy_loose_contract_still_holds() -> None:
    """Pre-ADR-0049 tests assert ``"field" in intent.subject.lower()``
    for ``"Why does the field diverge?"`` — ADR-0049 tightens the
    subject to ``"field"``, which still satisfies the substring check."""
    intent = classify_intent("Why does the field diverge?")
    assert intent.tag is IntentTag.CAUSE
    assert "field" in intent.subject.lower()
    assert intent.subject == "field"


# ---------------------------------------------------------------------------
# Pack-grounded path end-to-end — ADR-0049 unblocks ADR-0048 cases
# ---------------------------------------------------------------------------


def test_pack_grounded_surface_lifts_with_article_stripped() -> None:
    """``What is a procedure?`` was previously routed to the universal
    disclosure because the subject ``"a procedure"`` did not match the
    pack lemma index.  Post-ADR-0049 the article is stripped and the
    pack-grounded surface engages."""
    from chat.runtime import ChatRuntime

    rt = ChatRuntime()
    resp = rt.chat("What is a procedure?")
    assert resp.grounding_source == "pack"
    # Case-insensitive: gloss-backed surfaces capitalize the lemma
    # at sentence start (Procedure is ...).
    assert "procedure" in resp.surface.lower()
