"""W20F10 stale-fail / readiness-gate projection-sync helper 단위 검증.

배경:
    `ref_image_gen` 이 과거에 `episode.status='error'` 상태를 보고 분석 실패로
    failed row 를 남긴 뒤, 실제 checkpoint projection 이 회복되어 status 가
    `analyzed` 가 되었음에도 readiness gate 가 stale DB 문자열만 신뢰해 막혔다.

    수정 방향: `check_ref_images_ready` / `check_scene_images_ready` 진입부에서
    `ensure_analysis_projection_current` 를 호출하여 checkpoint → DB projection
    을 최신화한 뒤 Episode 를 다시 조회한다. sync 자체가 실패하면 fail-closed.

두 가지 deterministic 시나리오만 검증한다 (broad pytest 금지, focused only):
  1. stale Episode.status='error' 인데도 sync 가 status 를 'analyzed' 로
     끌어올리면 gate 가 통과한다.
  2. sync 가 RuntimeError 로 실패하면 `gate.projection_sync_failed`
     AppError 가 raise 되고, Episode.status 는 그대로 stale 상태로 남는다.
"""
from __future__ import annotations

import uuid

import pytest

pytestmark = pytest.mark.pg


def _seed_episode_with_entity_link(session, *, status: str):
    """gate 통과에 필요한 최소 시드 — project + episode + canon + link."""
    pid = f"w20f10-{uuid.uuid4()}"
    eid = f"ep-{uuid.uuid4()}"
    canon_id = f"canon-{uuid.uuid4()}"
    uid = f"user-{uuid.uuid4()}"
    now = "2026-05-28T00:00:00+00:00"

    from sqlalchemy import text as sql_text

    session.execute(sql_text(
        "INSERT INTO user_account (id, username, display_name, password_hash, "
        "role, is_active, created_at, updated_at) VALUES "
        "(:uid, :uname, 't', 'x', 'creator', 1, :now, :now)"
    ), {"uid": uid, "uname": f"u_{uid}", "now": now})
    session.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES (:pid, 'w20f10', :uid, :now, :now)"
    ), {"pid": pid, "uid": uid, "now": now})
    session.execute(sql_text(
        "INSERT INTO episode (id, project_id, episode_number, title, "
        "source_filename, source_path, fulltext, language, status, "
        "created_at, updated_at) VALUES (:eid, :pid, 1, 'ep1', 'fx.pdf', "
        "'/tmp/fx.pdf', 'fixture body', 'ko', :status, :now, :now)"
    ), {"eid": eid, "pid": pid, "status": status, "now": now})
    session.execute(sql_text(
        "INSERT INTO entity_canon (id, project_id, entity_type, name, "
        "metadata_json, status, created_at, updated_at) VALUES "
        "(:cid, :pid, 'character', 'C01', '{}', 'active', :now, :now)"
    ), {"cid": canon_id, "pid": pid, "now": now})
    session.execute(sql_text(
        "INSERT INTO entity_episode_link (id, project_id, canon_id, episode_id, "
        "source, t2i_appearance_count) VALUES (:lid, :pid, :cid, :eid, 'extracted', 0)"
    ), {"lid": str(uuid.uuid4()), "pid": pid, "cid": canon_id, "eid": eid})
    session.commit()
    return pid, eid


def test_check_ref_images_ready_resyncs_stale_episode_status(pg_session, monkeypatch):
    """stale `status='error'` 인데 sync 호출 후 'analyzed' 가 되면 gate 통과."""
    from app.core import pipeline_gate
    from sqlalchemy import text as sql_text

    pid, eid = _seed_episode_with_entity_link(pg_session, status="error")

    sync_calls = []

    def _fake_sync(project_id, episode_id, db_session, **_kw):
        sync_calls.append((project_id, episode_id))
        db_session.execute(
            sql_text("UPDATE episode SET status = 'analyzed' WHERE id = :eid"),
            {"eid": episode_id},
        )
        db_session.commit()
        return {}

    monkeypatch.setattr(
        "app.services.checkpoint_sync.orchestrate_full_sync",
        _fake_sync,
        raising=False,
    )

    pipeline_gate.check_ref_images_ready(pg_session, pid, eid)

    assert sync_calls == [(pid, eid)], "ensure helper must invoke sync exactly once"
    final_status = pg_session.execute(
        sql_text("SELECT status FROM episode WHERE id = :eid"), {"eid": eid},
    ).scalar()
    assert final_status == "analyzed"


def test_check_ref_images_ready_fails_closed_on_sync_error(pg_session, monkeypatch):
    """sync 가 raise 하면 gate.projection_sync_failed 로 fail-closed."""
    from app.core import pipeline_gate
    from app.core.errors import AppError
    from sqlalchemy import text as sql_text

    pid, eid = _seed_episode_with_entity_link(pg_session, status="error")

    def _broken_sync(*_args, **_kw):
        raise RuntimeError("checkpoint disk corrupted")

    monkeypatch.setattr(
        "app.services.checkpoint_sync.orchestrate_full_sync",
        _broken_sync,
        raising=False,
    )

    with pytest.raises(AppError) as exc_info:
        pipeline_gate.check_ref_images_ready(pg_session, pid, eid)
    assert exc_info.value.code == "gate.projection_sync_failed"

    stale_status = pg_session.execute(
        sql_text("SELECT status FROM episode WHERE id = :eid"), {"eid": eid},
    ).scalar()
    assert stale_status == "error", "fail-closed: stale status must remain"
