"""생성 확정이 **바깥 실행기까지** 살아남는가 (Codex BLOCK 1 · 2026-09-08).

## 무엇이 결함이었나

`_run_group` 이 「이건 지어낸 것이라 사진을 안 산다」로 확정해도, 바깥
`_execute` 는 그 값을 여전히 실패·미완료로 다뤘다. 경계가 셋 새 있었다:

  ① 좁힘 재시도 — `if rec.status != "ok"` 라서 생성 확정도 **한 번 더**
     브리핑을 샀다. 둘째 답이 외부 정본이나 실패로 바뀌면 첫 확정이
     뒤집혀 실제 검색이나 그룹 실패까지 간다.
  ② `_close_round` — 생성 확정을 저장·마감하지 않고 그냥 돌아갔다.
     라운드가 열린 채 남아 **재개가 판정을 매번 다시 산다.**
  ③ `_reusable` — `status != "ok"` 이면 무조건 False 인 데다 사진 파일과
     sha 까지 요구했다. 생성 확정에는 파일이 없다.

`completed_count` 에 생성을 더한 것만으로는 이 셋이 안 닫힌다.

## 여기서 재는 것 — `_run_group` 이 아니라 `_execute`

앞 시험(`test_visual_authority_decides_before_buying`)은 `_run_group` 만
불러서 이 셋을 하나도 못 본다. 그래서 여기서는 **바깥 실행기**를 태우고
유료 경로가 **몇 번 불렸는지 횟수로** 확인한다.

★가짜 사진 자산을 만들지 않는다. 생성 확정은 사진 칸이 **없어야** 하고,
 라운드 종착도 `FINALIZED`(자산 있음)가 아니라 `RESOLVED_NO_REF` 다.
"""
from __future__ import annotations

from typing import Any, Dict

from app.modules.pipeline.form_ref_rounds import RoundState, list_rounds

# 같은 판을 두 번 짓지 않는다 — 라운드 배선 시험의 대역을 그대로 쓴다.
from tests.core.test_form_reference_round_wiring import (  # noqa: F401
    GID,
    _Wired,
    stub_run_group,
    wired,
)

GENERATIVE = "generative"


def _stub_generative(w: _Wired, monkeypatch, *,
                     second_answer: str = GENERATIVE) -> None:
    """1차는 생성 확정. 2차가 불리면 `second_answer` 로 답한다.

    ★2차의 답을 **다르게** 둘 수 있어야 한다 — 「불리기만 하면 첫 확정이
     뒤집힌다」를 보이려면 뒤집히는 답을 준비해 둬야 하기 때문이다.
    """
    def _fake(*, group_id: str, start_index: int = 1,
              narrow: bool = False, **_ignored: Any) -> Dict[str, Any]:
        n = len(w.calls) + 1
        w.calls.append({"group_id": group_id, "narrow": narrow,
                        "start_index": start_index})
        status = GENERATIVE if n == 1 else second_answer
        if status == "failed":
            return {"status": "failed", "group_id": group_id,
                    "error": "sample fixture failure", "candidate_count": 0,
                    "audit": {"pack_version": "sample-fixture-pack"}}
        return {"status": GENERATIVE, "group_id": group_id,
                "visual_authority": "generative_authority",
                "authority_reason_ko": "이 이야기가 지어낸 것이다",
                "candidate_count": 0,
                "audit": {"pack_version": "sample-fixture-pack",
                          "verdict": {"visual_authority":
                                      "generative_authority"}}}

    monkeypatch.setattr(w.step, "_run_group", _fake)


# ── ① 좁힘 재시도를 안 탄다 ──────────────────────────────────────────

def test_생성_확정은_좁혀_다시_묻지_않는다(wired, monkeypatch):
    """★유료 브리핑이 **한 번만** 나가야 한다."""
    _stub_generative(wired, monkeypatch)

    out = wired.run()

    assert len(wired.calls) == 1, (
        f"생성 확정인데 유료 경로가 {len(wired.calls)}회 불렸다 — "
        f"{wired.calls}")
    assert out["completed_count"] == 1 and out["failed_count"] == 0


# ── ② 라운드가 사진 없이 durable 하게 닫힌다 ─────────────────────────

def test_라운드가_사진_없는_종착으로_닫힌다(wired, monkeypatch):
    _stub_generative(wired, monkeypatch)

    wired.run()

    rounds = list_rounds(wired.group_dir)
    assert len(rounds) == 1, f"라운드가 {len(rounds)}개다"
    r = rounds[0]
    assert r.state == RoundState.RESOLVED_NO_REF, (
        f"라운드가 {r.state.value} 로 남았다 — 열린 채면 재개가 다시 산다")
    assert not r.has_reference
    # ★가짜 자산·가짜 파일이 없어야 한다.
    for key in ("form_ref_path", "form_ref_sha256", "form_ref_asset_id"):
        assert not r.result.get(key), f"{key} 가 생성 확정에 들어 있다"


# ── ③ 재개가 판정을 다시 사지 않는다 ─────────────────────────────────

def test_정상_재개가_생성_확정을_되쓴다_유료_0회(wired, monkeypatch):
    """★★★핵심. CP 와 journal 이 둘 다 있는 **보통 재개** — `_reusable` 이
    지키는 자리다. 여기서 새면 이 그룹은 재개마다 판정을 다시 산다."""
    _stub_generative(wired, monkeypatch)
    first = wired.run_and_save_cp()
    assert len(wired.calls) == 1

    wired.calls.clear()
    second = wired.run_and_save_cp()

    assert wired.calls == [], (
        f"재개가 생성 확정을 다시 샀다 — {wired.calls}")
    assert second["completed_count"] == first["completed_count"] == 1
    assert ((second.get("data") or second)["groups"][GID].get("reused") is True), (
        "CP 재사용 경로를 안 지났다 — `_reusable` 이 아니라 다른 길로 왔다")


def test_CP_가_날아가도_라운드로_되쓴다_유료_0회(wired, monkeypatch):
    """CP 만 없어진 재개 — journal 이 권위라 유료 0회여야 한다."""
    _stub_generative(wired, monkeypatch)
    wired.run()
    wired.cp = None                      # CP 만 날린다 (라운드는 남는다)

    wired.calls.clear()
    out = wired.run()

    assert wired.calls == [], f"CP 만 없는데 다시 샀다 — {wired.calls}"
    assert out["completed_count"] == 1


# ── 대조 — 막은 것을 넓히지 않았다 ───────────────────────────────────
#
# ★위의 「생성 → 1회」와 아래 「실패 → 2회」가 **같은 실행기를 지나는 한 쌍**
#  이다. 하나만 보면 「재시도가 통째로 죽은 것」과 구별되지 않는다.

def test_외부_참조_실패는_종전처럼_좁혀_재시도한다(wired, monkeypatch):
    """★생성만 막았지 실패까지 막지 않았다 — 넓히면 회귀다."""
    stub_run_group(wired, monkeypatch, fail_attempts=1)

    wired.run()

    assert len(wired.calls) == 2, (
        f"실패 뒤 좁힘 재시도가 사라졌다 — {wired.calls}")
    assert wired.calls[1]["narrow"] is True
