"""Phase C — Task C2: registered pose guide scope (role 교정 + metadata).

build_registered_pose_guide 가 생성하는 두 중간물을 generation_context scope 안에서
영속화한다:
  - underlay(make_registration_underlay, PIL) → role=registered_pose_underlay (Phase B)
  - guide(generate_floor_plan_image, gpt-edit) → role=registered_pose_guide
    (★현재 default 'floor_plan_image' 오라벨 교정) + pipeline_metadata{group_id,bg_key,variant}.
input_image_ids 는 v1 defer(None — bg plate UUID worker DB 금지).

scope 미개방(flag OFF/단독) 시 capture 0(byte-identical). cache_hit 시 generate 미호출
→ guide 중복 capture 0(멱등). 생성물 품질은 실측 canary 영역 — 여기선 결정론 계약만.

PG theroad_test, uuid 격리. gpt-image 는 RecordingClient(실 API 0).
"""
import base64
import io
import json
import uuid
from datetime import datetime, timezone
from types import SimpleNamespace

from PIL import Image

import app.core.file_paths as fp
from app.core.database import SessionLocal
from app.models.catalog import ProjectRegistry, UserAccount
from app.models.project import Episode, ImageAsset
from app.services import registered_pose_guide_service as rpg
from app.services.image_capture.context import generation_context


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _seed_base(db) -> tuple[str, str]:
    uid = str(uuid.uuid4())
    db.add(UserAccount(
        id=uid, username=f"u_{uid[:8]}", display_name="t", password_hash="x",
        role="admin", created_at=_now(), updated_at=_now(),
    ))
    db.commit()
    pid = str(uuid.uuid4())
    db.add(ProjectRegistry(
        id=pid, name="Phase C2", created_by=uid, created_at=_now(), updated_at=_now(),
    ))
    eid = str(uuid.uuid4())
    db.add(Episode(
        id=eid, project_id=pid, episode_number=1, title="Pilot",
        source_filename="p.pdf", source_path="p.pdf", created_at=_now(), updated_at=_now(),
    ))
    db.commit()
    return pid, eid


def _png_bytes(color=(120, 120, 120), size=(1024, 1024)) -> bytes:
    buf = io.BytesIO()
    Image.new("RGB", size, color).save(buf, format="PNG")
    return buf.getvalue()


_BIG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 8192  # decode 후 >1024


class _RecordingClient:
    def __init__(self):
        b64 = base64.b64encode(_BIG).decode("ascii")

        class _Images:
            def edit(self, **kwargs):
                return SimpleNamespace(data=[SimpleNamespace(b64_json=b64)])

            def generate(self, **kwargs):
                return SimpleNamespace(data=[SimpleNamespace(b64_json=b64)])

        self.images = _Images()


_ANCHOR = {
    "character_short_id": "C04",
    "subject_state": "dead",
    "shared_state_contract": "the immobilized body sits slumped against the bed edge.",
    "locked_elements": [{"description": "the right hand rests on the bed surface"}],
}


def _stub_paths(monkeypatch, tmp_path):
    from app.core import config as _cfg
    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path / "projects"))
    monkeypatch.setattr(fp, "PROJECT_ROOT", tmp_path)


def _intermediate_rows(pid):
    db = SessionLocal()
    try:
        return db.query(ImageAsset).filter(
            ImageAsset.project_id == pid, ImageAsset.is_intermediate == True  # noqa: E712
        ).all()
    finally:
        db.close()


def test_guide_and_underlay_captured_with_correct_roles(monkeypatch, tmp_path):
    db = SessionLocal()
    try:
        pid, eid = _seed_base(db)
    finally:
        db.close()
    _stub_paths(monkeypatch, tmp_path)

    with generation_context(pid, eid, stage="registered_pose_guide",
                            still_id="still-pose-1", scene_index=12, shot_index=8):
        png, diag = rpg.build_registered_pose_guide(
            group_id="vca-s12-c04-immobilized", subject_anchor=_ANCHOR,
            env_bg_bytes=_png_bytes(), bg_key="L04B05",
            cache_dir=tmp_path / "guides", openai_client=_RecordingClient(),
        )
    assert diag["status"] == "generated"
    assert png

    rows = _intermediate_rows(pid)
    roles = {r.pipeline_role for r in rows}
    assert roles == {"registered_pose_underlay", "registered_pose_guide"}

    guide = next(r for r in rows if r.pipeline_role == "registered_pose_guide")
    assert guide.stage == "registered_pose_guide"
    assert guide.still_id == "still-pose-1"
    # input_image_ids v1 defer — bg plate UUID 미해결 → None(억지 주입 금지)
    assert guide.input_image_ids is None
    pm = json.loads(guide.pipeline_metadata_json)
    assert pm["group_id"] == "vca-s12-c04-immobilized"
    assert pm["bg_key"] == "L04B05"
    assert pm["scene_index"] == 12  # C0 fold


def test_no_capture_outside_scope_byte_identical(monkeypatch, tmp_path):
    db = SessionLocal()
    try:
        pid, eid = _seed_base(db)
    finally:
        db.close()
    _stub_paths(monkeypatch, tmp_path)

    # scope 미개방(flag OFF 상황의 attach no-op 등가) → capture 0
    png, diag = rpg.build_registered_pose_guide(
        group_id="g-noscope", subject_anchor=_ANCHOR,
        env_bg_bytes=_png_bytes(), bg_key="L04B05",
        cache_dir=tmp_path / "guides2", openai_client=_RecordingClient(),
    )
    assert png and diag["status"] == "generated"
    assert _intermediate_rows(pid) == []  # production 행 무변


def test_cache_hit_does_not_recapture_guide(monkeypatch, tmp_path):
    db = SessionLocal()
    try:
        pid, eid = _seed_base(db)
    finally:
        db.close()
    _stub_paths(monkeypatch, tmp_path)

    cache_dir = tmp_path / "guides3"
    with generation_context(pid, eid, stage="registered_pose_guide",
                            still_id="s1", scene_index=12, shot_index=8):
        rpg.build_registered_pose_guide(
            group_id="g-idem", subject_anchor=_ANCHOR, env_bg_bytes=_png_bytes(),
            bg_key="L04B05", cache_dir=cache_dir, openai_client=_RecordingClient())
        # 같은 scope 2nd build = cache_hit → underlay/guide 모두 미생성 → capture 0 추가
        rpg.build_registered_pose_guide(
            group_id="g-idem", subject_anchor=_ANCHOR, env_bg_bytes=_png_bytes(),
            bg_key="L04B05", cache_dir=cache_dir, openai_client=_RecordingClient())

    rows = _intermediate_rows(pid)
    # 1차 build 의 underlay+guide 2행만 — 2차 cache_hit 는 추가 0
    assert len(rows) == 2
