"""Phase 3.3 + 3.4 — NARRATIVE and EXAMPLE intent + composer tests.

The contracts pinned here:

  NARRATIVE
  - "Tell me about X" / "Describe X" / "What can you say about X"
    classify as NARRATIVE before falling through to DEFINITION.
  - Composer walks every reviewed chain rooted on X across all
    registered teaching corpora; emits up to max_clauses unique
    (predicate, object) clauses; deterministic ordering.
  - Falls through to OOV invitation when X is unknown.

  EXAMPLE
  - "Give me an example of X" / "Show an instance of X" /
    "Example of X" classify as EXAMPLE before DEFINITION.
  - Composer surfaces chains where X is the OBJECT (reverse-chain
    access pattern); dedupes by subject; deterministic ordering.
  - Falls through to OOV invitation when X is unknown.

  Both
  - Surface composes only pack atoms + verbatim chain content +
    fixed template — no content synthesis.
  - Tagged ``grounding_source="teaching"`` (same provenance as
    teaching_grounded_surface — both consume the reviewed corpora).
"""

from __future__ import annotations

import pytest

from chat.example_surface import example_grounded_surface
from chat.narrative_surface import narrative_grounded_surface
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
from generate.intent import IntentTag, classify_intent


# ---------------------------------------------------------------------------
# Intent classification
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("prompt", [
    "Tell me about light.",
    "Tell me about parent",
    "Describe truth",
    "Describe photosynthesis.",
    "What can you say about wisdom?",
    "What do you know about memory?",
])
def test_narrative_patterns_classify_narrative(prompt: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.NARRATIVE
    assert intent.subject


@pytest.mark.parametrize("prompt", [
    "Give me an example of truth.",
    "Show me an instance of knowledge.",
    "Show an example of parent.",
    "Example of meaning",
])
def test_example_patterns_classify_example(prompt: str) -> None:
    intent = classify_intent(prompt)
    assert intent.tag is IntentTag.EXAMPLE
    assert intent.subject


def test_narrative_pattern_precedes_definition() -> None:
    """``What can you say about X?`` could match the generic
    ``what is/are X`` pattern — assert NARRATIVE wins on the more
    specific pattern."""
    intent = classify_intent("What can you say about light?")
    assert intent.tag is IntentTag.NARRATIVE


# ---------------------------------------------------------------------------
# NARRATIVE composer — pure function
# ---------------------------------------------------------------------------


def test_narrative_aggregates_multiple_chains() -> None:
    """``truth`` appears as the subject of multiple cognition chains;
    the narrative composer emits a clause for each."""
    surface = narrative_grounded_surface("truth")
    assert surface is not None
    assert "narrative-grounded (cognition_chains_v1)" in surface
    assert "truth grounds knowledge" in surface
    assert "truth requires evidence" in surface


def test_narrative_dedupes_by_predicate_object() -> None:
    """When cause + verification carry the same (connective, object),
    only one clause is emitted."""
    surface = narrative_grounded_surface("light")
    assert surface is not None
    # (light, cause, reveals, truth) + (light, verification, reveals, truth)
    # → one clause "light reveals truth", not two.
    assert surface.count("light reveals truth") == 1


def test_narrative_handles_relations_pack_subject() -> None:
    surface = narrative_grounded_surface("parent")
    assert surface is not None
    # ADR-0067 — ``parent`` is the subject of both the in-pack chain
    # ``parent precedes child`` (relations_chains_v1) and the cross-
    # pack chain ``parent grounds understanding`` (cross_pack_chains_v1).
    # The narrative composer aggregates both; the corpus tag reflects
    # both binding sources.
    assert "relations_chains_v1" in surface
    assert "parent precedes child" in surface


def test_narrative_handles_relations_v2_subject() -> None:
    surface = narrative_grounded_surface("mother")
    assert surface is not None
    assert "narrative-grounded (relations_chains_v2)" in surface
    assert "mother precedes daughter" in surface


def test_narrative_unknown_lemma_returns_none() -> None:
    assert narrative_grounded_surface("photosynthesis") is None
    assert narrative_grounded_surface("xyzunknown") is None


def test_narrative_empty_input_returns_none() -> None:
    assert narrative_grounded_surface("") is None
    assert narrative_grounded_surface("   ") is None


def test_narrative_is_deterministic() -> None:
    a = narrative_grounded_surface("truth")
    b = narrative_grounded_surface("truth")
    assert a == b


def test_narrative_max_clauses_caps_output() -> None:
    """``max_clauses=1`` should emit just the lexicographically-first
    clause for a multi-chain subject."""
    full = narrative_grounded_surface("truth", max_clauses=8)
    capped = narrative_grounded_surface("truth", max_clauses=1)
    assert full is not None
    assert capped is not None
    assert capped != full
    assert len(capped) < len(full)


# ---------------------------------------------------------------------------
# EXAMPLE composer — pure function
# ---------------------------------------------------------------------------


def test_example_surfaces_reverse_chain() -> None:
    """``truth`` appears as the object of ``light reveals truth`` —
    the example composer surfaces the chain inverted (X = object)."""
    surface = example_grounded_surface("truth")
    assert surface is not None
    assert "example-grounded (cognition_chains_v1)" in surface
    assert "light reveals truth" in surface


def test_example_aggregates_multiple_subjects() -> None:
    """``knowledge`` appears as the object of multiple chains; the
    example composer dedupes by subject."""
    surface = example_grounded_surface("knowledge")
    assert surface is not None
    # truth/understanding/evidence all relate to knowledge as object.
    assert "knowledge" in surface
    # Each is listed once at most.
    subjects = ["truth", "understanding", "evidence"]
    found = [s for s in subjects if f"{s}" in surface]
    assert len(found) >= 1


def test_example_handles_relations_object() -> None:
    """``parent`` appears as object of ``child follows parent`` +
    ``family grounds parent`` — multiple examples.  ADR-0067 added
    ``understanding requires parent`` (cross-pack), which is also
    aggregated; the corpus tag widens to reflect both bindings."""
    surface = example_grounded_surface("parent")
    assert surface is not None
    assert "relations_chains_v1" in surface
    assert "parent" in surface


def test_example_unknown_object_returns_none() -> None:
    assert example_grounded_surface("photosynthesis") is None
    assert example_grounded_surface("xyzunknown") is None


def test_example_is_deterministic() -> None:
    a = example_grounded_surface("truth")
    b = example_grounded_surface("truth")
    assert a == b


def test_example_max_examples_caps_output() -> None:
    capped = example_grounded_surface("knowledge", max_examples=1)
    full = example_grounded_surface("knowledge", max_examples=8)
    assert capped is not None
    assert full is not None
    assert len(capped) <= len(full)


# ---------------------------------------------------------------------------
# Live runtime — NARRATIVE
# ---------------------------------------------------------------------------


def test_runtime_narrative_on_known_subject_routes_to_teaching() -> None:
    """Composer-level invariant: NARRATIVE intent on a pack-resident
    subject routes to the teaching-grounded composer and the provenance
    tag ``narrative-grounded`` lands on the surface.

    Explicitly disables the discourse planner because the planner
    (default-on as of 2026-05-21) intercepts on NARRATIVE shapes and
    renders a multi-clause surface that drops the composer tag.  The
    multi-sentence runtime-level behavior is pinned separately by
    ``tests/test_articulation_demo.py``.
    """
    rt = ChatRuntime(config=RuntimeConfig(discourse_planner=False))
    resp = rt.chat("Tell me about truth.")
    assert resp.grounding_source == "teaching"
    assert "narrative-grounded" in resp.surface
    assert "truth" in resp.surface


def test_runtime_narrative_on_oov_routes_to_oov_invitation() -> None:
    rt = ChatRuntime()
    resp = rt.chat("Describe photosynthesis.")
    assert resp.grounding_source == "oov"
    assert "photosynthesis" in resp.surface
    assert "PackMutationProposal" in resp.surface


# ---------------------------------------------------------------------------
# Live runtime — EXAMPLE
# ---------------------------------------------------------------------------


def test_runtime_example_on_known_object_routes_to_teaching() -> None:
    """Composer-level invariant for EXAMPLE intent on a pack-resident
    object.  Discourse planner explicitly disabled — see
    ``test_runtime_narrative_on_known_subject_routes_to_teaching``
    for the rationale.
    """
    rt = ChatRuntime(config=RuntimeConfig(discourse_planner=False))
    resp = rt.chat("Give me an example of truth.")
    assert resp.grounding_source == "teaching"
    assert "example-grounded" in resp.surface
    assert "light reveals truth" in resp.surface


def test_runtime_example_on_oov_routes_to_oov_invitation() -> None:
    rt = ChatRuntime()
    resp = rt.chat("Example of photosynthesis")
    assert resp.grounding_source == "oov"


def test_runtime_example_on_relations_object() -> None:
    """Composer-level invariant — EXAMPLE intent on a relations-pack
    object surfaces the relations corpus tag.  Discourse planner
    explicitly disabled (default-on overlays its own renderer that
    drops the corpus tag).
    """
    rt = ChatRuntime(config=RuntimeConfig(discourse_planner=False))
    resp = rt.chat("Give me an example of parent.")
    assert resp.grounding_source == "teaching"
    assert "relations_chains_v1" in resp.surface
