"""FINDING 6 W4a (e2e-bughunt-v1) — consumer-boundary visible normalization.

scene_detail 11-shot crash 중 6 shot 의 root cause = shot_staging 이 emit 한
per-shot structured artifact (subject_reference_policy / frame_spatial_contract)
가 shot_director.visible_entity_ids SOT 와 불일치 — depicted-but-not-physically-
present entity (사진/CCTV/반사/침입손) + shot-level visibility narrowing.

`filter_subject_reference_policy_to_visible` + `filter_fsc_constraints_to_visible`
= render_prompt_card consumer-boundary 에서 non-shot-visible 항목을 deterministic
drop. malformed/shape-invalid 은 보존 → 기존 validator 가 그대로 fail-fast.

no live LLM + NO VLM — 순수 helper 함수 검증.
"""
from __future__ import annotations

import pytest

from app.core.errors import AppError
from app.core.frame_spatial_contract import (
    filter_fsc_constraints_to_visible,
    validate_and_prepare,
)
from app.core.subject_reference_policy import (
    filter_subject_reference_policy_to_visible,
    normalize_subject_reference_policy_items,
)


def _srp(subject_id: str, policy: str = "base_id_required") -> dict:
    return {
        "subject_id": subject_id,
        "policy_type": "identity_reference",
        "policy": policy,
        "reason": "test reason",
    }


def _fsc(target_kind: str, target_id: str, label: str) -> dict:
    return {
        "target_kind": target_kind,
        "target_id": target_id,
        "label": label,
        "screen_zone": "upper_center",
        "depth_plane": "foreground",
        "gesture_action": "none",
        "gesture_target_label": "",
    }


# ── G1 — subject_reference_policy filter: non-visible subject drop ──
def test_g1_srp_filter_drops_non_visible_subject():
    # C03 = depicted-but-not-present (사진 hallucination), C01 = visible.
    items = [_srp("C03"), _srp("C01")]
    filtered = filter_subject_reference_policy_to_visible(
        items, {"C01"}, where="g1",
    )
    assert [i["subject_id"] for i in filtered] == ["C01"]
    # filter 후 normalize 가 unknown_subject 미raise.
    policy_map = normalize_subject_reference_policy_items(
        filtered, visible_subject_ids={"C01"}, where="g1",
    )
    assert set(policy_map) == {"C01"}


def test_g1_srp_filter_empty_when_all_non_visible():
    # S20.3 패턴 — visible_subject_ids 가 빈 set (CCTV 모니터 shot).
    items = [_srp("C11")]
    filtered = filter_subject_reference_policy_to_visible(
        items, set(), where="g1",
    )
    assert filtered == []
    # 빈 list → normalize 는 {} 반환 (required field 부재 아님).
    assert normalize_subject_reference_policy_items(
        filtered, visible_subject_ids=set(), where="g1",
    ) == {}


def test_g1_srp_filter_no_drop_when_all_visible():
    items = [_srp("C01", "base_id_required"),
             _srp("C02", "id_and_outlook_required")]
    filtered = filter_subject_reference_policy_to_visible(
        items, {"C01", "C02"}, where="g1",
    )
    assert len(filtered) == 2


def test_g1_srp_filter_none_passthrough():
    assert filter_subject_reference_policy_to_visible(None, {"C01"}) is None


# ── G2 — FSC filter: non-visible target drop ──
def test_g2_fsc_filter_drops_non_visible_character_target():
    # S10.4 패턴 — C08 (침입하는 손) 이 visible_entities 밖.
    contract = {
        "reason": "movement_direction",
        "constraints": [_fsc("character", "C08", "intruding hand"),
                        _fsc("character", "C07", "Korean man")],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07", "L03"])
    assert [c["target_id"] for c in filtered["constraints"]] == ["C07"]
    # filter 후 validate_and_prepare 가 fsc_cross_check_failed 미raise.
    prepared = validate_and_prepare(filtered, ["C07", "L03"])
    assert len(prepared["constraints"]) == 1


def test_g2_fsc_filter_drops_non_visible_prop_target():
    contract = {
        "reason": "points_to_anchor",
        "constraints": [_fsc("prop", "P09", "absent prop"),
                        _fsc("prop", "P03", "visible prop")],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07", "P03"])
    assert [c["target_id"] for c in filtered["constraints"]] == ["P03"]


# ── G3 — preserve visible character/prop + background constraints ──
def test_g3_fsc_filter_preserves_background_and_visible():
    contract = {
        "reason": "movement_direction",
        "constraints": [_fsc("character", "C07", "visible man"),
                        _fsc("background", "", "iron door seam")],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07"])
    assert len(filtered["constraints"]) == 2
    kinds = {c["target_kind"] for c in filtered["constraints"]}
    assert kinds == {"character", "background"}


# ── G4 — all FSC constraints dropped → None (valid no-contract) ──
def test_g4_fsc_filter_all_dropped_returns_none():
    contract = {
        "reason": "movement_direction",
        "constraints": [_fsc("character", "C08", "hand"),
                        _fsc("character", "C09", "other")],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07"])
    assert filtered is None
    # None 은 validate_and_prepare 의 valid no-contract pass-through.
    assert validate_and_prepare(None, ["C07"]) is None


def test_g4_fsc_filter_none_passthrough():
    assert filter_fsc_constraints_to_visible(None, ["C07"]) is None


# ── G5 — malformed / shape-invalid preserved → existing validator fail-fast ──
def test_g5_srp_malformed_preserved_failfast():
    # malformed subject_id (C## shape 아님) — filter 가 보존 → normalize fail-fast.
    items = [{"subject_id": "XYZ", "policy_type": "identity_reference",
              "policy": "base_id_required", "reason": "x"}]
    filtered = filter_subject_reference_policy_to_visible(
        items, {"C01"}, where="g5",
    )
    assert filtered == items  # 보존 (drop 안 함)
    with pytest.raises(AppError) as exc:
        normalize_subject_reference_policy_items(
            filtered, visible_subject_ids={"C01"}, where="g5",
        )
    assert exc.value.code.startswith("step.contract_violation.subject_reference_policy")


def test_g5_fsc_malformed_target_preserved_failfast():
    # malformed character target_id — filter 보존 → validate_and_prepare fail-fast
    # 으로 fsc_invalid (cross_check_failed 아님).
    contract = {
        "reason": "movement_direction",
        "constraints": [_fsc("character", "BADID", "x")],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07"])
    assert filtered == contract  # malformed 은 drop 대상 아님 → 보존
    with pytest.raises(AppError) as exc:
        validate_and_prepare(filtered, ["C07"])
    assert exc.value.code == "render_prompt_card.fsc_invalid"


# ── G6 — mixed FSC: drop invalid, keep valid + background ──
def test_g6_fsc_filter_mixed_drop_invalid_keep_valid_and_background():
    contract = {
        "reason": "movement_direction",
        "constraints": [
            _fsc("character", "C08", "intruding hand"),  # non-visible → drop
            _fsc("character", "C07", "visible man"),     # visible → keep
            _fsc("background", "", "door seam"),         # background → keep
        ],
    }
    filtered = filter_fsc_constraints_to_visible(contract, ["C07", "L03"])
    pairs = {(c["target_kind"], c["target_id"]) for c in filtered["constraints"]}
    assert ("character", "C08") not in pairs
    assert ("character", "C07") in pairs
    assert ("background", "") in pairs
    prepared = validate_and_prepare(filtered, ["C07", "L03"])
    assert len(prepared["constraints"]) == 2
