"""Phase 0 reference-necessity audit — pure 계산 + fail-closed GATE 검증.

GATE predicate 는 materialization risk 기준 (2026-05-23 erratum, Codex 검토):
prompt-ID occurrence 자체는 diagnostic, blocking 은 required_refs 밖
recurring character(visible_shot_count>=2) 뿐. location/outlook/prop/
one-shot character 의 prompt-ID 등장은 warning bucket.
"""
import json

from app.services.reference_necessity_audit import compute_reference_usage_report

_ALL = ("shot_selection", "shot_director", "scene_director",
        "shot_validator", "scene_detail")


def _write(root, step_id, data, status="completed"):
    d = root / step_id
    d.mkdir(parents=True, exist_ok=True)
    (d / "manifest.json").write_text(
        json.dumps({"status": status, "data": data}), encoding="utf-8",
    )


def _full_checkpoints(tmp_path, *, scene_detail_overrides=None):
    """5종 필수 입력을 전부 정상 상태로 생성. 루트 반환."""
    root = tmp_path / "episodes" / "ep1"
    _write(root, "scene_director",
           {"scenes": [{"scene_index": 1, "present_entity_ids": ["C04"]}]})
    _write(root, "shot_validator",
           {"scenes": [{"scene_index": 1, "shots": [
               {"shot_index": 0, "character_ids": ["C04"]}]}]})
    _write(root, "shot_director",
           {"scenes": [{"scene_index": 1, "shots": [
               {"shot_index": 0, "visible_entity_ids": ["C04", "C01"]}]}]})
    _write(root, "shot_selection",
           {"scenes": [{"scene_index": 1, "selected_shot_indices": [0]}]})
    sd = scene_detail_overrides or {"scenes": [{"scene_index": 1,
        "_shot_index": 0, "visible_entities": ["C01"],
        "render_prompt_card": {"asset_requirements": {"required_refs": [
            {"kind": "character", "id": "C01"}]}},
        "t2i_variations": [{"t2i_prompt": "C01 stands alone"}]}]}
    _write(root, "scene_detail", sd)
    return root


def _checkpoints(tmp_path, *, scene_director=None, shot_validator=None,
                 shot_director=None, shot_selection=None, scene_detail=None):
    """5종 입력 체크포인트를 인자별로 작성. 미지정 인자는 최소 유효 기본값."""
    root = tmp_path / "episodes" / "ep1"
    _write(root, "scene_director", scene_director or
           {"scenes": [{"scene_index": 1, "present_entity_ids": []}]})
    _write(root, "shot_validator", shot_validator or
           {"scenes": [{"scene_index": 1, "shots": [
               {"shot_index": 0, "character_ids": []}]}]})
    _write(root, "shot_director", shot_director or
           {"scenes": [{"scene_index": 1, "shots": [
               {"shot_index": 0, "visible_entity_ids": []}]}]})
    _write(root, "shot_selection", shot_selection or
           {"scenes": [{"scene_index": 1, "selected_shot_indices": [0]}]})
    _write(root, "scene_detail", scene_detail or
           {"scenes": [{"scene_index": 1,
               "render_prompt_card": {"asset_requirements":
                                      {"required_refs": []}},
               "t2i_variations": []}]})
    return root


def test_audit_set_is_broad_union_minus_required_refs(tmp_path):
    root = _full_checkpoints(tmp_path)
    report = compute_reference_usage_report(
        checkpoints_dir=root,
        entity_catalog={
            "C01": {"name": "주인공", "entity_type": "character"},
            "C04": {"name": "김형사", "entity_type": "character"},
        },
        generated_ref_counts={"C01": 1, "C04": 2},
    )
    assert report["gate"]["passed"] is True
    assert set(report["safety_diff"]["required_refs_only"]) == {"C01"}
    assert set(report["safety_diff"]["audit_set"]) == {"C04"}
    assert report["safety_diff"]["audit_set_with_t2i_usage"] == []
    assert report["safety_diff"]["gate_blocking_ids"] == []


def test_gate_fails_on_recurring_character_missing_required_ref(tmp_path):
    """recurring character (selected shot 2개 등장 → visible_shot_count=2) 가
    required_refs 밖인데 t2i_prompt 에 ID 로 쓰이면 GATE FAIL (진짜 SOT gap)."""
    root = _checkpoints(
        tmp_path,
        shot_director={"scenes": [{"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C07"]},
            {"shot_index": 1, "visible_entity_ids": ["C07"]},
        ]}]},
        shot_selection={"scenes": [
            {"scene_index": 1, "selected_shot_indices": [0, 1]}]},
        scene_detail={"scenes": [{"scene_index": 1,
            "render_prompt_card": {"asset_requirements": {"required_refs": []}},
            "t2i_variations": [{"t2i_prompt": "C07 enters the room"}]}]},
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root,
        entity_catalog={"C07": {"name": "남자", "entity_type": "character"}},
        generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert "C07" in report["safety_diff"]["gate_blocking_ids"]


def test_location_prompt_id_is_warning_not_blocking(tmp_path):
    """location ID 가 t2i_prompt 에 등장해도 GATE blocking 아님 — out_of_scope
    warning bucket 으로만 분류 (background 경로는 이번 scope 밖)."""
    root = _checkpoints(
        tmp_path,
        shot_director={"scenes": [{"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C01"]}]}]},
        shot_selection={"scenes": [
            {"scene_index": 1, "selected_shot_indices": [0]}]},
        scene_detail={"scenes": [{"scene_index": 1,
            "visible_entities": ["L01"],
            "render_prompt_card": {"asset_requirements": {"required_refs": [
                {"kind": "character", "id": "C01"}]}},
            "t2i_variations": [{"t2i_prompt": "C01 stands in L01 alley"}]}]},
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is True
    assert report["safety_diff"]["gate_blocking_ids"] == []
    assert "L01" in (report["safety_diff"]["warnings"]
                     ["out_of_scope_prompt_id_ids"])


def test_prop_prompt_id_is_warning_not_blocking(tmp_path):
    """prop ID 가 t2i_prompt 에 등장하고 required_ref 가 없어도 GATE blocking
    아님 — required_refs(kind=prop) 가 단일 SOT, prompt_id_only_prop warning."""
    root = _checkpoints(
        tmp_path,
        shot_director={"scenes": [{"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C01"]}]}]},
        shot_selection={"scenes": [
            {"scene_index": 1, "selected_shot_indices": [0]}]},
        scene_detail={"scenes": [{"scene_index": 1,
            "visible_entities": ["P01"],
            "render_prompt_card": {"asset_requirements": {"required_refs": [
                {"kind": "character", "id": "C01"}]}},
            "t2i_variations": [{"t2i_prompt": "C01 holds P01 phone"}]}]},
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is True
    assert report["safety_diff"]["gate_blocking_ids"] == []
    assert "P01" in (report["safety_diff"]["warnings"]
                     ["prompt_id_only_prop_ids"])


def test_one_shot_character_prompt_id_is_warning_not_blocking(tmp_path):
    """one-shot character (selected shot 1개) 가 required_refs 밖이고 t2i 에
    ID 로 등장해도 GATE blocking 아님 — Phase 2 text_only 정정 대상 warning."""
    root = _checkpoints(
        tmp_path,
        shot_director={"scenes": [{"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C01", "C08"]}]}]},
        shot_selection={"scenes": [
            {"scene_index": 1, "selected_shot_indices": [0]}]},
        scene_detail={"scenes": [{"scene_index": 1,
            "render_prompt_card": {"asset_requirements": {"required_refs": [
                {"kind": "character", "id": "C01"}]}},
            "t2i_variations": [{"t2i_prompt": "C01 and C08 in frame"}]}]},
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is True
    assert report["safety_diff"]["gate_blocking_ids"] == []
    assert "C08" in (report["safety_diff"]["warnings"]
                     ["expected_text_only_prompt_id_ids"])


def test_gate_fails_on_missing_required_input(tmp_path):
    """필수 입력 5종 중 하나라도 manifest 부재 → 무조건 FAIL."""
    root = _full_checkpoints(tmp_path)
    import shutil
    shutil.rmtree(root / "shot_director")
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert report["gate"]["input_status"]["shot_director"] == "missing"


def test_gate_fails_on_not_completed_status(tmp_path):
    """manifest status != completed (partial 등) → FAIL."""
    root = _full_checkpoints(tmp_path)
    _write(root, "scene_detail", {"scenes": [{"scene_index": 1}]},
           status="partial")
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert report["gate"]["input_status"]["scene_detail"] == "not_completed:partial"


def test_gate_fails_on_selected_map_scene_missing(tmp_path):
    """shot_director 에 있는 scene 이 shot_selection 에 없으면 FAIL (fallback 금지)."""
    root = _full_checkpoints(tmp_path)
    # shot_director 에 scene 2 추가, shot_selection 에는 없음
    _write(root, "shot_director", {"scenes": [
        {"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C01"]}]},
        {"scene_index": 2, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C07"]}]},
    ]})
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert any("scene 2" in r for r in report["gate"]["fail_reasons"])


def test_gate_fails_on_missing_status(tmp_path):
    """manifest 에 top-level status 키가 아예 없으면 FAIL (fail-closed —
    StepRunner 는 항상 status 를 쓰므로 누락 = malformed checkpoint)."""
    root = _full_checkpoints(tmp_path)
    # scene_detail manifest 를 status 키 없이 덮어쓴다
    (root / "scene_detail" / "manifest.json").write_text(
        json.dumps({"data": {"scenes": [{"scene_index": 1}]}}),
        encoding="utf-8",
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert report["gate"]["input_status"]["scene_detail"] == "not_completed:missing"


def test_gate_fails_on_selected_shot_missing(tmp_path):
    """selected_shot_indices 의 shot index 가 shot_director.shots 에 없으면
    FAIL — visible_shot_count undercount 위험 (fallback 금지)."""
    root = _checkpoints(
        tmp_path,
        shot_director={"scenes": [{"scene_index": 1, "shots": [
            {"shot_index": 0, "visible_entity_ids": ["C01"]}]}]},
        shot_selection={"scenes": [
            {"scene_index": 1, "selected_shot_indices": [0, 1]}]},
        scene_detail={"scenes": [{"scene_index": 1,
            "render_prompt_card": {"asset_requirements": {"required_refs": []}},
            "t2i_variations": []}]},
    )
    report = compute_reference_usage_report(
        checkpoints_dir=root, entity_catalog={}, generated_ref_counts={},
    )
    assert report["gate"]["passed"] is False
    assert any("selected shot [1]" in r
               for r in report["gate"]["fail_reasons"])
