"""Phase C — Task C1: zoom continuity crop scope (dedup 경계 + input_image_ids).

핵심 결정론 계약 (Decision #1/#3/#4):
  - crop(:320-340)만 generation_context scope 안 → image_asset intermediate 1행
    (pipeline_role=zoom_continuity_crop, input_image_ids=[source_asset_id]).
  - fill(:342+ gemini)은 scope **밖** → 최종 scene asset 이 intermediate 로 중복
    안 됨(gemini 호출 시점 current_context()==None 으로 잠금).
  - source_asset_id None 이면 input_image_ids 미주입(비차단, 부분 lineage 허용).
  - _load_source_primary_bytes 는 (bytes, asset_id) 반환(단건 path UUID resolve).

VLM/viability 는 stub(이미지 품질은 실측 canary 영역). PG theroad_test, uuid 격리.
"""
import io
import json
import uuid
from datetime import datetime, timezone

from PIL import Image

import app.core.file_paths as fp
import app.services.zoom_continuity_render_service as zc
from app.core.database import SessionLocal
from app.models.catalog import ProjectRegistry, UserAccount
from app.models.project import Episode, ImageAsset, SceneStill
from app.services.image_capture.context import current_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 C1", 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(w=1280, h=720) -> bytes:
    buf = io.BytesIO()
    Image.new("RGB", (w, h), (40, 40, 40)).save(buf, "PNG")
    return buf.getvalue()


def _stub_internals(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)
    monkeypatch.setattr(zc, "vlm_locate_bbox",
                        lambda *a, **k: {"found": True, "bbox": [0.2, 0.2, 0.5, 0.5]})
    monkeypatch.setattr(zc, "assess_viability",
                        lambda bbox: {"found": True, "area_ratio": 0.09,
                                      "verdict": "ok", "note": ""})


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()


class _FillRecordingGemini:
    """fill 시점의 generation_context 상태를 기록 — dedup 경계 잠금용."""

    def __init__(self):
        self.fill_ctx = "UNSET"

    def set_context(self, **kw):
        return self

    def generate_image(self, prompt, labeled_references=None):
        self.fill_ctx = current_context()
        return (b"FILL" + b"\x00" * 2000, 5)


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

    gem = _FillRecordingGemini()
    png, prompt, diag = zc.generate_continuity_crop_png(
        project_id=pid, episode_id=eid, scene_index=12, shot_index=15,
        zoom_target={"group_id": "g1", "source": [12, 9], "locked_elements": []},
        gemini_client=gem, source_bytes=_png_bytes(),
        still_id="still-zoom-1", source_asset_id="src-asset-1",
    )
    assert png == b"FILL" + b"\x00" * 2000  # fill 결과 반환(최종물)

    rows = _intermediate_rows(pid)
    assert len(rows) == 1  # crop 만 — fill 은 scope 밖이라 추가 0
    row = rows[0]
    assert row.pipeline_role == "zoom_continuity_crop"
    assert row.stage == "zoom_continuity"
    assert row.still_id == "still-zoom-1"
    assert row.is_intermediate is True
    assert json.loads(row.input_image_ids) == ["src-asset-1"]  # 구조 UUID lineage
    pm = json.loads(row.pipeline_metadata_json)
    assert pm["scene_index"] == 12  # C0 fold
    assert pm["group_id"] == "g1"

    # ★dedup 경계: production gemini_image_client 는 generate_image 안에서
    # capture_generated_image 를 호출하지만, fill 이 scope 밖이라 그 시점 ctx 가
    # None → 최종 scene asset 이 intermediate 로 중복 저장되지 않는다.
    assert gem.fill_ctx is None


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

    gem = _FillRecordingGemini()
    png, _prompt, _diag = zc.generate_continuity_crop_png(
        project_id=pid, episode_id=eid, scene_index=3, shot_index=1,
        zoom_target={"group_id": "g2", "source": [3, 0], "locked_elements": []},
        gemini_client=gem, source_bytes=_png_bytes(),
        still_id="still-zoom-2", source_asset_id=None,
    )
    assert png  # 진행됨(비차단)
    rows = _intermediate_rows(pid)
    assert len(rows) == 1
    # source_asset_id None → input_image_ids 미주입(None), 억지 빈 주입 금지
    assert rows[0].input_image_ids is None


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

    db = SessionLocal()
    try:
        pid, eid = _seed_base(db)
        src_still_id = str(uuid.uuid4())
        db.add(SceneStill(
            id=src_still_id, project_id=pid, episode_id=eid,
            scene_index=12, shot_index=9, still_index=0, created_at=_now(),
        ))
        # primary scene asset + 실파일
        img_dir = tmp_path / "projects" / pid / "scene"
        img_dir.mkdir(parents=True, exist_ok=True)
        img_file = img_dir / "src.png"
        img_file.write_bytes(_png_bytes())
        asset_id = str(uuid.uuid4())
        db.add(ImageAsset(
            id=asset_id, project_id=pid, episode_id=eid, asset_type="scene",
            still_id=src_still_id, is_primary=1, file_path=str(img_file),
            status="generated", created_at=_now(),
        ))
        db.commit()

        out_bytes, out_asset_id = zc._load_source_primary_bytes(
            db, pid, eid, (12, 9))
        assert out_bytes == _png_bytes()
        assert out_asset_id == asset_id
    finally:
        db.close()
