"""표적 씬 실행 scope 결정론 계약 (2026-07-23 슬라이스 E, Codex 설계 합의).

순수 계층 전수: 파싱/requested fail-fast/그래프·prev 체인 클로저/effective
prev 해석/audit 영속. 시나리오 의존 0 — SAMPLE id·정수 index 만.
"""
import pytest

from app.modules.pipeline.scene_image_scope import (
    build_effective_prev_map,
    check_scope_universe,
    build_scope_audit,
    closure_over_index_graph,
    closure_over_prev_chain,
    load_scope_audit,
    parse_target_scenes,
    requested_still_ids,
    save_scope_audit,
    scope_audit_path,
)


def test_parse_target_scenes_normalizes():
    assert parse_target_scenes("") == ()
    assert parse_target_scenes(None) == ()
    assert parse_target_scenes(" 10, 1,14 ,10 ") == (1, 10, 14)


def test_parse_target_scenes_fail_closed():
    with pytest.raises(ValueError):
        parse_target_scenes("1,abc")
    with pytest.raises(ValueError):
        parse_target_scenes("-3")
    with pytest.raises(ValueError):
        parse_target_scenes(", ,")  # 비어 있지 않은데 유효 0개


def _stills():
    return [
        {"id": "st_a", "scene_index": 1, "shot_index": 1},
        {"id": "st_b", "scene_index": 1, "shot_index": 2},
        {"id": "st_c", "scene_index": 2, "shot_index": 1},
        {"id": "st_d", "scene_index": 3, "shot_index": 1},
    ]


def test_requested_still_ids_and_fail_fast():
    assert requested_still_ids(_stills(), (1, 3)) == ["st_a", "st_b", "st_d"]
    # 표적 씬에 still 0개 = fail-fast (오타/재세그먼트 mismatch)
    with pytest.raises(ValueError, match="fail-fast"):
        requested_still_ids(_stills(), (1, 9))


def test_closure_over_index_graph_recursive_and_sibling_excluded():
    # deps: d(3)→c(2)→a(0), b(1)=비의존 sibling
    deps = {3: {2}, 2: {0}, 0: set(), 1: set()}
    effective, added = closure_over_index_graph(_stills(), ["st_d"], deps)
    assert effective == ["st_a", "st_c", "st_d"]
    assert added == ["st_a", "st_c"]
    assert "st_b" not in effective  # sibling 미포함


def test_closure_over_index_graph_multi_edge_union():
    # legacy 최종 그래프=base+zoom+composition+immobilized 합산 — 클로저는
    # 병합 후 단일 그래프의 고정점 (edge 출처 무관 동일 처리)
    deps = {3: {1, 2}, 2: set(), 1: {0}, 0: set()}
    effective, added = closure_over_index_graph(_stills(), ["st_d"], deps)
    assert effective == ["st_a", "st_b", "st_c", "st_d"]
    assert set(added) == {"st_a", "st_b", "st_c"}


def test_closure_over_index_graph_missing_requested_fail_closed():
    with pytest.raises(ValueError, match="fail-closed"):
        closure_over_index_graph(_stills(), ["st_zzz"], {})


def test_closure_over_prev_chain_recursive():
    prev_of = {"S3sh1": "S2sh1", "S2sh1": "S1sh1"}
    known = {"S1sh1", "S2sh1", "S3sh1", "S9sh9"}
    effective, added = closure_over_prev_chain(["S3sh1"], prev_of, known)
    assert effective == ["S1sh1", "S2sh1", "S3sh1"]
    assert added == ["S1sh1", "S2sh1"]
    # 비의존 sibling 미포함
    assert "S9sh9" not in effective


def test_closure_over_prev_chain_cycle_and_unknown_fail_closed():
    with pytest.raises(ValueError, match="cycle"):
        closure_over_prev_chain(
            ["S2sh1"], {"S2sh1": "S1sh1", "S1sh1": "S2sh1"},
            {"S1sh1", "S2sh1"})
    with pytest.raises(ValueError, match="우주 밖"):
        closure_over_prev_chain(
            ["S2sh1"], {"S2sh1": "S0sh0"}, {"S2sh1"})


def test_build_effective_prev_map_share_plan_authority():
    classify = {
        "S2sh1": {"prev": "S1sh1"},
        "S3sh1": {"prev": "S2sh1"},
        "S4sh1": {"prev": "S3sh1"},
        "S5sh1": {},
    }
    share = {
        # ref_plan=background → prev 미사용
        "S3sh1": {"ref_plan": "background"},
        # ref_plan=prev → 계획 앵커로 override
        "S4sh1": {"ref_plan": "prev", "prev_anchor_tag": "S1sh1"},
    }
    out = build_effective_prev_map(list(classify), classify, share)
    assert out["S2sh1"] == "S1sh1"      # 계획 없음=classify 유지
    assert "S3sh1" not in out           # background=prev 제거
    assert out["S4sh1"] == "S1sh1"      # prev 지휘=앵커 override
    assert "S5sh1" not in out
    # ref_plan=prev 인데 앵커 결손 = fail-closed (apply_share_plan_prev)
    with pytest.raises(ValueError):
        build_effective_prev_map(
            ["S2sh1"], {"S2sh1": {"prev": "S1sh1"}},
            {"S2sh1": {"ref_plan": "prev"}})


def test_scope_audit_roundtrip_and_fail_closed(tmp_path):
    path = scope_audit_path(tmp_path, "SAMPLE_P", "SAMPLE_E")
    assert path.name == "scene_target_scope.json"
    audit = build_scope_audit(
        target_scenes=(3, 1),
        requested_ids=["st_b", "st_a"],
        dependency_added_ids=["st_c"],
        path_kind="recipe",
    )
    assert audit["requested_scenes"] == [1, 3]
    assert audit["effective_ids"] == ["st_a", "st_b", "st_c"]
    save_scope_audit(path, audit)
    assert not path.with_suffix(".json.tmp").exists()  # atomic replace
    loaded = load_scope_audit(path, expected_scenes=(1, 3))
    assert loaded["effective_ids"] == ["st_a", "st_b", "st_c"]
    assert loaded["dependency_added"] == ["st_c"]
    # 부재 = fail-closed
    with pytest.raises(ValueError, match="audit 부재"):
        load_scope_audit(
            scope_audit_path(tmp_path, "OTHER", "E"), expected_scenes=(1,))
    # 형상 오류 = fail-closed
    bad = scope_audit_path(tmp_path, "BAD_P", "E")
    bad.parent.mkdir(parents=True, exist_ok=True)
    bad.write_text("{}", encoding="utf-8")
    with pytest.raises(ValueError, match="contract_version"):
        load_scope_audit(bad, expected_scenes=(1,))


def _saved_audit(tmp_path, **over):
    import json as _json

    audit = build_scope_audit(
        target_scenes=(1, 3), requested_ids=["st_a", "st_b"],
        dependency_added_ids=["st_c"], path_kind="recipe")
    audit.update(over)
    path = scope_audit_path(tmp_path, "SAMPLE_P", "SAMPLE_E")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(_json.dumps(audit, ensure_ascii=False),
                    encoding="utf-8")
    return path


def test_load_scope_audit_rejects_stale_or_malformed(tmp_path):
    """Codex 재리뷰 HIGH-3: 다른 표적의 stale audit·빈 목록·버전/종류
    오류·집합 비정합 전부 fail-closed."""
    path = _saved_audit(tmp_path)
    # 정상 통과 기준선
    assert load_scope_audit(path, expected_scenes=(3, 1))
    # 다른 표적의 stale audit
    with pytest.raises(ValueError, match="stale"):
        load_scope_audit(path, expected_scenes=(2,))
    # contract_version 불일치
    _saved_audit(tmp_path, contract_version=99)
    with pytest.raises(ValueError, match="contract_version"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # path_kind enum 밖
    _saved_audit(tmp_path, path_kind="other")
    with pytest.raises(ValueError, match="path_kind"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # requested 빈 목록 (expected 대조 전에 형상부터 잠금)
    _saved_audit(tmp_path, requested_ids=[], effective_ids=["st_c"],
                 dependency_added=["st_c"])
    with pytest.raises(ValueError, match="requested_ids 빈 목록"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # requested⊄effective
    _saved_audit(tmp_path, effective_ids=["st_c"],
                 dependency_added=["st_c"])
    with pytest.raises(ValueError, match="requested⊄effective"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # requested∪dependency != effective
    _saved_audit(tmp_path, effective_ids=["st_a", "st_b", "st_c", "st_d"])
    with pytest.raises(ValueError, match="!= effective"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # 중복 id
    _saved_audit(tmp_path, requested_ids=["st_a", "st_a"])
    with pytest.raises(ValueError, match="중복"):
        load_scope_audit(path, expected_scenes=(1, 3))
    # 비문자열 id
    _saved_audit(tmp_path, requested_ids=["st_a", 3])
    with pytest.raises(ValueError, match="형상 오류"):
        load_scope_audit(path, expected_scenes=(1, 3))


def test_check_scope_universe_couples_audit_to_current_stills():
    """재재리뷰 HIGH-1: audit 를 현재 selected-still 우주와 결합 —
    (a)같은 씬 stale old id (b)표적 씬 선택 샷 일부 누락 (c)unknown
    dependency 전부 fail-closed."""
    audit = build_scope_audit(
        target_scenes=(1,), requested_ids=["st_1"],
        dependency_added_ids=["st_dep"], path_kind="recipe")
    # 정상: 우주와 정확 일치
    check_scope_universe(audit, {"st_1": 1, "st_dep": 2, "st_x": 3})
    # (a) 스틸 세대 교체 — 같은 씬 번호에 새 id
    with pytest.raises(ValueError, match="우주와 불일치"):
        check_scope_universe(audit, {"new_still": 1, "st_dep": 2})
    # (b) 표적 씬 선택 샷 일부 누락 (우주엔 2개, audit 은 1개)
    with pytest.raises(ValueError, match="우주와 불일치"):
        check_scope_universe(
            audit, {"st_1": 1, "st_1b": 1, "st_dep": 2})
    # (c) unknown dependency id
    with pytest.raises(ValueError, match="unknown dependency"):
        check_scope_universe(audit, {"st_1": 1})
