"""합성 이미지가 **제 화 아래에** 기록되는가 (2026-09-04).

## 무엇이 결함이었나 — Codex BLOCK

`reference_composite_service.generate_composite_image` 가 저장 화를 이렇게
골랐다:

    link = db.query(EntityEpisodeLink).filter(
        EntityEpisodeLink.canon_id == character_id).first()
    episode_id = link.episode_id if link else "shared"

**임의의 첫 행**이다 — 프로젝트 필터도 정렬도 없었다. 공유 인물 `C01` 이
1·2화에 다 있으면 **2화 옷의 합성이 1화 폴더·DB·trace 에 기록된다.**
화 삭제·감사·자산 계보가 통째로 어긋난다.

## 여기서 재는 것

1. 부른 쪽이 화를 주면 **그 화**로 간다
2. 안 주고 링크가 **딱 하나**면 그 화 (종전 동작과 같다)
3. 여러 화면 **안 고르고** `shared`
4. 엔티티 단위 재생성이 각 배정의 **제 화**를 넘긴다 — 같은 인물이
   EP1=O01 · EP2=O02 면 두 호출의 저장 화가 각각 EP1·EP2

★1~3 은 저장 자리를 정하는 **순수 판단**이라 이미지 생성 없이 잰다.
 4 는 재생성 루프가 화를 흘리는지를 호출 인자로 잰다.

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

import uuid

import pytest
from sqlalchemy import text as sql_text

pytestmark = pytest.mark.pg


def _seed(session, pid: str, *eids: str) -> None:
    uid = f"comp-{uuid.uuid4()}"
    session.execute(sql_text(
        "INSERT INTO user_account (id, username, display_name, password_hash, "
        "role, is_active, created_at, updated_at) VALUES "
        "(:u, :un, 't', 'x', 'creator', 1, 'x', 'x')"), {"u": uid, "un": uid})
    session.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, "
        "updated_at) VALUES (:p, 'comp', :u, 'x', 'x')"), {"p": pid, "u": uid})
    for n, eid in enumerate(eids, start=1):
        session.execute(sql_text(
            "INSERT INTO episode (id, project_id, title, episode_number, "
            "source_filename, source_path, status, created_at, updated_at) "
            "VALUES (:e, :p, :t, :n, 'x', 'x', 'analyzed', 'x', 'x')"),
            {"e": eid, "p": pid, "t": f"{n}화", "n": n})
    session.commit()


def _canon(session, pid: str, sid: str, name: str, etype: str) -> str:
    cid = str(uuid.uuid4())
    session.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 "
        "(:c, :p, :s, :n, :e, '', '[]', '{}', 't', 'active', 'x', 'x')"),
        {"c": cid, "p": pid, "s": sid, "n": name, "e": etype})
    return cid


def _link(session, pid: str, eid: str, cid: str) -> None:
    session.execute(sql_text(
        "INSERT INTO entity_episode_link (id, canon_id, project_id, episode_id) "
        "VALUES (:l, :c, :p, :e)"),
        {"l": str(uuid.uuid4()), "c": cid, "p": pid, "e": eid})


def _pair(session, pid: str, char_id: str, ol_id: str, eid) -> None:
    session.execute(sql_text(
        "INSERT INTO character_outlook (id, project_id, episode_id, "
        "character_id, outlook_id, created_at) VALUES (:i, :p, :e, :c, :o, 'x')"),
        {"i": str(uuid.uuid4()), "p": pid, "e": eid, "c": char_id, "o": ol_id})


def _svc(session, pid: str):
    """★프로덕션이 만드는 것과 **같은 방식**으로 만든다 — 손으로 조립하면
    생성자 계약이 바뀌어도 시험이 안 잡는다."""
    from app.services.reference_image_service import ReferenceImageService

    return ReferenceImageService(
        db=session, project_id=pid, actor_id="tester")._composite_svc


@pytest.fixture
def scene(pg_session):
    """공유 인물 C01 이 1·2화에 다 있고, 화마다 다른 옷."""
    pid = f"p-{uuid.uuid4()}"
    ep1, ep2 = str(uuid.uuid4()), str(uuid.uuid4())
    _seed(pg_session, pid, ep1, ep2)
    ids = {
        "C01": _canon(pg_session, pid, "C01", "정순", "character"),
        "O01": _canon(pg_session, pid, "O01", "앞치마", "outlook"),
        "O02": _canon(pg_session, pid, "O02", "외투", "outlook"),
    }
    for eid in (ep1, ep2):
        _link(pg_session, pid, eid, ids["C01"])
    _link(pg_session, pid, ep1, ids["O01"])
    _link(pg_session, pid, ep2, ids["O02"])
    _pair(pg_session, pid, ids["C01"], ids["O01"], ep1)
    _pair(pg_session, pid, ids["C01"], ids["O02"], ep2)
    pg_session.commit()
    return pid, ep1, ep2, ids


def test_부른_쪽이_준_화로_간다(pg_session, scene):
    pid, ep1, ep2, ids = scene
    svc = _svc(pg_session, pid)
    assert svc._storage_episode(ids["C01"], ep2) == ep2


def test_여러_화에_걸렸는데_안_주면_안_고른다(pg_session, scene):
    """★임의로 고르면 2화 옷이 1화 폴더에 들어간다."""
    pid, _ep1, _ep2, ids = scene
    svc = _svc(pg_session, pid)
    assert svc._storage_episode(ids["C01"], None) == "shared"


def test_링크가_하나면_그_화_종전_동작(pg_session, scene):
    pid, ep1, ep2, ids = scene
    pg_session.execute(sql_text(
        "DELETE FROM entity_episode_link WHERE project_id = :p AND canon_id = :c "
        "AND episode_id = :e"), {"p": pid, "c": ids["C01"], "e": ep2})
    pg_session.commit()
    svc = _svc(pg_session, pid)
    assert svc._storage_episode(ids["C01"], None) == ep1


def test_다른_프로젝트_링크를_집지_않는다(pg_session, scene):
    """앞 판은 `project_id` 필터조차 없었다."""
    pid, ep1, ep2, ids = scene
    other = f"p-{uuid.uuid4()}"
    other_ep = str(uuid.uuid4())
    _seed(pg_session, other, other_ep)
    # 같은 canon_id 로 남의 프로젝트에 링크를 심는다.
    _link(pg_session, other, other_ep, ids["C01"])
    pg_session.execute(sql_text(
        "DELETE FROM entity_episode_link WHERE project_id = :p AND canon_id = :c "
        "AND episode_id = :e"), {"p": pid, "c": ids["C01"], "e": ep2})
    pg_session.commit()

    svc = _svc(pg_session, pid)
    assert svc._storage_episode(ids["C01"], None) == ep1, (
        "남의 프로젝트 링크를 집었다")


def test_엔티티_재생성이_각_배정의_제_화를_넘긴다(pg_session, scene, monkeypatch):
    """★★★끝점 — 같은 인물이 EP1=O01 · EP2=O02 면 두 호출의 저장 화가 각각."""
    pid, ep1, ep2, ids = scene
    svc = _svc(pg_session, pid)

    seen: list = []

    def _fake(character_id, outlook_id, ip=None, episode_id=None):
        seen.append((outlook_id, episode_id))
        return {"ok": True}

    monkeypatch.setattr(svc, "generate_composite_image", _fake)
    svc.regenerate_composites_for_entity(ids["C01"])

    got = dict(seen)
    assert got.get(ids["O01"]) == ep1, f"1화 옷이 1화로 안 갔다: {seen}"
    assert got.get(ids["O02"]) == ep2, f"2화 옷이 2화로 안 갔다: {seen}"
