"""preflight 의 **모집단**이 프로덕션과 같은가 (2026-08-29 Codex BLOCK).

유료 이미지 경로는 `ScenePersistenceService.load_episode_still_dicts` 로
`is_selected` · `still_index>=0` · `status!='stale'` 를 걸러 굽는다.
preflight 가 에피소드의 `scene_still` 을 **전부** 읽으면, 굽지도 않을 행의
옛 배정 하나가 exit 1 을 내어 **멀쩡한 재주행을 거짓으로 막는다.**

그래서 여기서 재는 것은 「깨진 것을 잡는가」만이 아니라
**「안 굽는 것을 안 잡는가」**다 — 그쪽이 이번 BLOCK 의 본체다.

Lane: ``-m pg``.
"""
from __future__ import annotations

import json
import uuid
from pathlib import Path

import pytest
from sqlalchemy import text as sql_text

pytestmark = pytest.mark.pg

TOOLS = Path(__file__).resolve().parents[2] / "tools" / "prompt_measure"


def _evaluate(db, pid: str, eid: str) -> int:
    import importlib.util

    spec = importlib.util.spec_from_file_location(
        "_preflight_outfit", TOOLS / "preflight_outfit_resolution.py")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)          # type: ignore[union-attr]
    return mod.evaluate(db, pid, eid)


def _seed_project_episode(db, pid: str, eid: str) -> None:
    uid = f"preflight-{uuid.uuid4()}"
    db.execute(sql_text(
        "INSERT INTO user_account (id, username, display_name, password_hash, "
        "role, is_active, created_at, updated_at) VALUES "
        "(:uid, :un, 't', 'x', 'creator', 1, '2026-01-01', '2026-01-01')"
    ), {"uid": uid, "un": f"u_{uid}"})
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES (:pid, 'preflight', :uid, '2026-01-01', '2026-01-01')"
    ), {"pid": pid, "uid": uid})
    db.execute(sql_text(
        "INSERT INTO episode (id, project_id, title, episode_number, "
        "source_filename, source_path, created_at, updated_at) VALUES "
        "(:eid, :pid, '막차', 1, 'x.txt', 'x/x.txt', '2026-01-01', '2026-01-01')"
    ), {"eid": eid, "pid": pid})


def _canon_linked(db, pid: str, eid: str, short_id: str, name: str, etype: str) -> str:
    cid = str(uuid.uuid4())
    db.execute(sql_text(
        "INSERT INTO entity_canon (id, project_id, short_id, name, entity_type, "
        "description, stable_traits, metadata_json, t2i_prompt, status, "
        "created_at, updated_at) VALUES "
        "(:cid, :pid, :s, :n, :e, '', '[]', '{}', '', 'active', "
        "'2026-01-01', '2026-01-01')"
    ), {"cid": cid, "pid": pid, "s": short_id, "n": name, "e": etype})
    db.execute(sql_text(
        "INSERT INTO entity_episode_link (id, canon_id, project_id, episode_id) "
        "VALUES (:lid, :cid, :pid, :eid)"
    ), {"lid": str(uuid.uuid4()), "cid": cid, "pid": pid, "eid": eid})
    return cid


def _still(db, pid: str, eid: str, *, idx: int, scene: int, shot: int,
           outlook_short: str | None, selected: bool = True,
           status: str = "active") -> None:
    """실물 shape 그대로 — `t2i_variations_json[*].outfit_assignments`."""
    variations = [{
        "variant_label": "var_1",
        "t2i_prompt": "…",
        "outfit_assignments": ([] if outlook_short is None else
                               [{"character_id": "C01",
                                 "outlook_id": outlook_short}]),
    }]
    db.execute(sql_text(
        "INSERT INTO scene_still (id, project_id, episode_id, still_index, "
        "scene_index, shot_index, is_selected, status, t2i_variations_json, "
        "created_at) VALUES "
        "(:id, :pid, :eid, :i, :sc, :sh, :sel, :st, :v, '2026-01-01')"
    ), {"id": str(uuid.uuid4()), "pid": pid, "eid": eid, "i": idx,
        "sc": scene, "sh": shot, "sel": selected, "st": status,
        "v": json.dumps(variations, ensure_ascii=False)})


@pytest.fixture
def episode(pg_session):
    pid, eid = f"p-{uuid.uuid4()}", str(uuid.uuid4())
    _seed_project_episode(pg_session, pid, eid)
    _canon_linked(pg_session, pid, eid, "C01", "정임", "character")
    _canon_linked(pg_session, pid, eid, "O01", "감색차장제복", "outlook")
    pg_session.commit()
    return pid, eid


def test_굽는_샷이_다_풀리면_통과(pg_session, episode):
    pid, eid = episode
    _still(pg_session, pid, eid, idx=0, scene=1, shot=1, outlook_short="O01")
    pg_session.commit()
    assert _evaluate(pg_session, pid, eid) == 0


def test_안_굽는_행이_깨져_있어도_통과한다(pg_session, episode):
    """★이번 BLOCK 의 본체 — 미선택·stale·음수 index 는 프로덕션이 안 읽는다."""
    pid, eid = episode
    _still(pg_session, pid, eid, idx=0, scene=1, shot=1, outlook_short="O01")
    _still(pg_session, pid, eid, idx=1, scene=1, shot=2,
           outlook_short="O99", selected=False)          # 미선택인데 깨짐
    _still(pg_session, pid, eid, idx=2, scene=1, shot=3,
           outlook_short="O99", status="stale")          # stale 인데 깨짐
    _still(pg_session, pid, eid, idx=-1, scene=1, shot=4,
           outlook_short="O99")                          # 음수 index 인데 깨짐
    pg_session.commit()
    assert _evaluate(pg_session, pid, eid) == 0, (
        "안 굽는 행 때문에 멀쩡한 주행을 막았다")


def test_굽는_샷이_깨져_있으면_막는다(pg_session, episode):
    """양성 확인 — 진짜 결함은 그대로 잡아야 한다."""
    pid, eid = episode
    _still(pg_session, pid, eid, idx=0, scene=1, shot=1, outlook_short="O01")
    _still(pg_session, pid, eid, idx=1, scene=1, shot=8, outlook_short="O02")
    pg_session.commit()
    assert _evaluate(pg_session, pid, eid) == 1


def test_배정이_하나도_없으면_통과가_아니라_미확정(pg_session, episode):
    """0 을 「다 풀렸다」로 읽지 않는다."""
    pid, eid = episode
    _still(pg_session, pid, eid, idx=0, scene=1, shot=1, outlook_short=None)
    pg_session.commit()
    assert _evaluate(pg_session, pid, eid) == 2


def test_굽을_샷이_없으면_미확정(pg_session, episode):
    pid, eid = episode
    assert _evaluate(pg_session, pid, eid) == 2
