"""재사용으로 끝나는 방문에서도 **CP 대표 = DB 대표** (2026-09-20 Codex BLOCK).

사람이 손으로 올리는 것은 **파이프라인 밖**이라(`image_upload_service`)
scene CP 를 안 고친다. 그래서 정상적인 기본 경로가 어긋난다:

    G 를 만들고 CP 대표 = G
    → 사람이 H 를 올린다 (DB 대표 = H, CP 는 그대로 G)
    → 입력 무변경 JIT 방문 → 재사용으로 `continue`
    → **CP 대표는 영영 G**

★새 그림을 사거나 G 를 다시 보관해서 풀 일이 아니다 — 메타 한 번만
 맞춘다. 그리고 **이미 맞으면 아무것도 안 쓴다**: 무변경 방문은
 기록도 남기지 않는 것이 계약이다.
"""
from __future__ import annotations

from pathlib import Path

import pytest

from app.services.still_recipe_service import _reconcile_cp_primary


class _CP:
    def __init__(self, rows=None):
        self.rows = dict(rows or {})
        self.writes = []

    def get_completed(self, item_id):
        row = self.rows.get(item_id)
        return dict(row) if isinstance(row, dict) else {}

    def mark_completed(self, item_id, result):
        self.rows[item_id] = dict(result)
        self.writes.append((item_id, dict(result)))


def _call(cp, *, held_id="H", held_path=Path("/x/h.png")):
    _reconcile_cp_primary(
        scene_cp=cp, still_id="S1", db=None, project_id="P",
        episode_id="E", held_id=held_id, held_path=held_path, tag="S1sh1")


def test_a_stale_checkpoint_primary_is_corrected_once():
    cp = _CP({"S1": {"primary_id": "G", "primary_path": "/x/g.png",
                     "asset_ids": ["G"], "recipe_tag": "S1sh1"}})

    _call(cp)

    assert cp.rows["S1"]["primary_id"] == "H", "CP 대표가 안 맞춰졌다"
    assert cp.rows["S1"]["primary_path"] == "/x/h.png"
    # ★다른 칸은 **안 버린다** — 대표만 맞춘다
    assert cp.rows["S1"]["asset_ids"] == ["G"]
    assert len(cp.writes) == 1


def test_a_second_unchanged_visit_writes_nothing():
    """★이미 맞으면 **아무것도 안 쓴다** — 무변경 방문은 무기록이 계약."""
    cp = _CP({"S1": {"primary_id": "G"}})

    _call(cp)                      # 첫 방문 — 맞춘다
    n_after_first = len(cp.writes)
    _call(cp)                      # 두 번째 무변경 방문
    _call(cp)                      # 세 번째

    assert n_after_first == 1
    assert len(cp.writes) == 1, (
        f"무변경 방문이 CP 를 또 썼다 — {len(cp.writes)}회")


def test_an_unreadable_checkpoint_writes_nothing():
    """못 읽으면 **안 쓴다** — 같은 기록을 되풀이 쓰는 쪽보다 낫다."""
    class _Boom(_CP):
        def get_completed(self, item_id):
            raise RuntimeError("못 읽는다")

    cp = _Boom({"S1": {"primary_id": "G"}})
    _call(cp)
    assert cp.writes == []


def test_a_missing_row_is_created_with_the_real_primary():
    cp = _CP({})
    _call(cp)
    assert cp.rows["S1"]["primary_id"] == "H"
    assert cp.rows["S1"]["recipe_tag"] == "S1sh1"


def test_both_reuse_branches_reconcile_and_carry_the_pipeline_asset():
    """**두 재사용 갈래 모두** 계보를 파이프라인 산출로 물리고 CP 를 맞춘다.

    한쪽만 고치면 기록이 바뀐 샷에서 사람 업로드가 prev 계보로 물린다.
    """
    import inspect
    import pathlib

    from app.services import still_recipe_service as mod

    src = pathlib.Path(inspect.getfile(mod)).read_text(encoding="utf-8")
    assert src.count("primary_asset_by_tag[tag] = _cmp_prior.id") == 2, (
        "재사용 갈래 둘이 다 파이프라인 산출을 계보로 쓰지 않는다")
    assert src.count("_reconcile_cp_primary(") == 3, (
        "정의 1 + 두 갈래 호출 2 가 아니다")
    assert "primary_asset_by_tag[tag] = _prior.id" not in src, (
        "사람 대표를 prev 계보로 물리는 자리가 남아 있다")


def test_a_stale_path_is_corrected_even_when_the_id_matches():
    """★ID 만 보면 **경로가 낡은 행**을 못 고친다 (Codex NON-BLOCK).

    이 helper 를 「대표 정합」이라 부르려면 두 칸이 다 맞아야 한다.
    """
    cp = _CP({"S1": {"primary_id": "H", "primary_path": "/old/h.png"}})

    _call(cp)

    assert cp.rows["S1"]["primary_path"] == "/x/h.png"
    assert len(cp.writes) == 1
    _call(cp)                                   # 이제는 둘 다 맞다
    assert len(cp.writes) == 1, "맞는데 또 썼다"


def test_the_log_does_not_claim_the_whole_visit_was_free():
    """★「이미지 호출 0」은 **이 helper 의 사실**이지 방문 전체가 아니다.

    둘째 호출부는 **유료 작업 뒤** 산출 bytes 가 같을 때도 온다.
    """
    import inspect

    from app.services.still_recipe_service import _reconcile_cp_primary as f

    src = inspect.getsource(f)
    assert "이미지 호출 0" not in src, (
        "로그가 방문 전체를 무구매로 읽히게 한다")
    assert "CP 메타만 정정" in src
