"""render_prompt_card episode_reference_policy overlay — text_only subject
required_ref 억제 + base_id_required materialization reconciliation."""
from app.core.steps.render_prompt_card import (
    build_render_prompt_card, compute_card_hash,
)

# ---------------------------------------------------------------------------
# Shared fixture helper
# ---------------------------------------------------------------------------

def _minimal_card_kwargs(visible, srp_items):
    """Return a dict of ALL kwargs required by build_render_prompt_card.

    Modelled after TestBuildRenderPromptCard.test_full_round_trip (line ~1060
    in tests/unit/test_render_prompt_card.py).

    ``visible`` — list of visible entity SIDs (e.g. ["C01O01", "C08O01"]).
                  Must use non-O00 outlook suffix so that id_and_outlook_required
                  subjects emit a character_outlook required_ref (O00 is excluded
                  by builder policy — resolver attaches as base, not outlook pair).
    ``srp_items`` — list of subject_reference_policy item dicts; stored in
                    staging["subject_reference_policy"] (required field).

    Uses background_mode_on=True + bg_id + used_outlook_pairs so that both
    character_outlook and background required_refs are emitted, making overlay
    suppression observable.
    """
    # Build outlook_pairs and used_outlook_pairs from the visible SIDs.
    # SID format: C##O## — split on last 'O' to get (character_id, outlook_id).
    outlook_pairs = []
    used_outlook_pairs = set()
    for sid in visible:
        if "O" in sid:
            idx = sid.rfind("O")
            char_id = sid[:idx]
            outlook_suffix = sid[idx:]   # e.g. "O01"
            outlook_pairs.append({"character_id": char_id, "outlook_id": outlook_suffix})
            used_outlook_pairs.add((char_id, outlook_suffix))

    return dict(
        scene_index=1,
        shot_index=1,
        seg={},
        shot_info={"camera_direction": "medium shot"},
        visible_entities=visible,
        outlook_pairs=outlook_pairs,
        perception_mode=None,
        staging={
            "camera_direction": "medium shot",
            "framing_scale": "medium",
            "lighting_mood": "warm dim",
            "key_bg_elements": [],
            "subject_reference_policy": srp_items,
        },
        bg_id="cb_main_room",
        bg_owned=[],
        bg_camera_meta={"camera_position": "wide"},
        bg_guide="guide text",
        is_close_framing=False,
        background_mode_on=True,
        fixed_elements=[],
        previous_shot_refs=[],
        forward_zoom_targets=[],
        used_outlook_pairs=used_outlook_pairs,
    )


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------

def _ref_ids(card):
    return [r.get("id") for r in card["asset_requirements"]["required_refs"]]


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

def test_text_only_char_suppressed_from_required_refs():
    """C08=text_only (staging 기본 id_and_outlook_required) → required_ref 제거.

    Uses C##O01 (non-O00) so that the default id_and_outlook_required policy
    emits a character_outlook required_ref for C08 in card_off, which is then
    suppressed by the episode_reference_policy overlay in card_on.
    """
    manifest = {"schema_version": 1, "policy": {
        "C08": {"mode": "text_only", "entity_type": "character",
                "reason": "low freq", "visible_shot_count": 1,
                "provisional": False}}}
    common = _minimal_card_kwargs(visible=["C01O01", "C08O01"], srp_items=[])
    card_off = build_render_prompt_card(**common, episode_reference_policy=None)
    card_on = build_render_prompt_card(
        **common, episode_reference_policy=manifest)
    assert any("C08" in str(i) for i in _ref_ids(card_off))
    assert not any("C08" in str(i) for i in _ref_ids(card_on))
    assert any("C01" in str(i) for i in _ref_ids(card_on))


def test_base_id_required_preserved_despite_text_only():
    """materialization reconciliation: text_only 여도 staging 이 C08 을
    base_id_required 로 지정했으면 required_ref(kind=character) 유지."""
    manifest = {"schema_version": 1, "policy": {
        "C08": {"mode": "text_only", "entity_type": "character",
                "reason": "low freq", "visible_shot_count": 1,
                "provisional": False}}}
    srp = [{"subject_id": "C08", "policy_type": "identity_reference",
            "policy": "base_id_required", "reason": "explicit partial-frame"}]
    common = _minimal_card_kwargs(visible=["C08O01"], srp_items=srp)
    card = build_render_prompt_card(**common, episode_reference_policy=manifest)
    assert any(r.get("id") == "C08" and r.get("kind") == "character"
               for r in card["asset_requirements"]["required_refs"])


def test_none_manifest_is_noop():
    common = _minimal_card_kwargs(visible=["C01O01"], srp_items=[])
    h1 = compute_card_hash(build_render_prompt_card(**common))
    h2 = compute_card_hash(
        build_render_prompt_card(**common, episode_reference_policy=None))
    assert h1 == h2


# ---------------------------------------------------------------------------
# W2: policy-conditional id_policy constraints
# ---------------------------------------------------------------------------

def _constraints_text(card) -> str:
    """Return all id_policy constraints joined for substring search."""
    return " ".join(card["id_policy"]["constraints"])


def test_w2_generic_descriptor_no_unconditional_c_id_instruction():
    """(a) When a subject is generic_descriptor_allowed, the card must NOT
    emit an unconditional 'visible real people ... use C##O##' instruction.

    Phase 2 overlay marks C08 as text_only which forces its staging policy to
    generic_descriptor_allowed in the resolver.  We also explicitly set the
    srp so build_id_policy sees the generic_descriptor_allowed policy directly.
    """
    manifest = {"schema_version": 1, "policy": {
        "C08": {"mode": "text_only", "entity_type": "character",
                "reason": "low freq", "visible_shot_count": 1,
                "provisional": False}}}
    srp = [{"subject_id": "C08", "policy_type": "identity_reference",
            "policy": "generic_descriptor_allowed", "reason": "low freq"}]
    common = _minimal_card_kwargs(visible=["C01O01", "C08O01"], srp_items=srp)
    card = build_render_prompt_card(**common, episode_reference_policy=manifest)
    joined = _constraints_text(card)
    # The unconditional blanket instruction must NOT appear verbatim.
    assert "visible real people with outfit binding use C##O## composite IDs" not in joined


def test_w2_generic_descriptor_explicit_prohibition():
    """(b) When a subject is generic_descriptor_allowed, constraints must
    explicitly say that subject must NOT use C## or C##O##.
    """
    srp = [{"subject_id": "C08", "policy_type": "identity_reference",
            "policy": "generic_descriptor_allowed", "reason": "low freq"}]
    common = _minimal_card_kwargs(visible=["C01O01", "C08O01"], srp_items=srp)
    card = build_render_prompt_card(**common, episode_reference_policy=None)
    joined = _constraints_text(card)
    # Must say generic_descriptor_allowed subjects must NOT use C## / C##O##.
    assert "generic_descriptor_allowed" in joined
    assert "must not use" in joined.lower() or "must NOT use" in joined


def test_w2_id_and_outlook_required_still_says_must_use_composite():
    """(c) id_and_outlook_required subjects must still get C##O## instruction."""
    # No generic subjects — default path.
    common = _minimal_card_kwargs(visible=["C01O01"], srp_items=[])
    card = build_render_prompt_card(**common, episode_reference_policy=None)
    joined = _constraints_text(card)
    # Must mention id_and_outlook_required → C##O## semantics.
    assert "id_and_outlook_required" in joined
    assert "C##O##" in joined


def test_w2_generic_descriptor_zero_required_refs_regression():
    """(d) Phase 2 invariant: generic_descriptor_allowed subjects emit zero
    required_refs entries (regression guard).
    """
    manifest = {"schema_version": 1, "policy": {
        "C08": {"mode": "text_only", "entity_type": "character",
                "reason": "low freq", "visible_shot_count": 1,
                "provisional": False}}}
    srp = [{"subject_id": "C08", "policy_type": "identity_reference",
            "policy": "generic_descriptor_allowed", "reason": "low freq"}]
    common = _minimal_card_kwargs(visible=["C01O01", "C08O01"], srp_items=srp)
    card = build_render_prompt_card(**common, episode_reference_policy=manifest)
    c08_refs = [r for r in card["asset_requirements"]["required_refs"]
                if "C08" in str(r.get("id", ""))]
    assert c08_refs == [], f"Expected no C08 required_refs, got {c08_refs}"
