"""`entity_t2i` 의 schema 올림은 **다시 사지 않는다** (2026-09-21).

## 왜 올렸나

`entity_detail` 한 묶음이 통째로 빈 채 끝나 48개 엔티티의 설명 칸이 비었고,
`entity_t2i` 완료 기록이 그 빈 칸을 그대로 안고 있다. 그 칸을 앞 단계
원자료(`entity_merge`)로 메우는 길을 냈는데, **완료된 CP 는 `SKIP` 에
걸려 그 코드를 아예 안 탄다**. 그래서 manifest 의 `schema_version` 을
올려 재평가가 `_execute(mode="resume")` 까지 오게 한다.

## 이 시험이 잠그는 것

올림이 **재구매로 번지지 않아야** 한다. `RERUN_SELF` 는 force 가 아니다 —
CP 도 하류도 안 지운다. 그 성질이 깨지면 219개를 다시 사고 하류까지
날아간다. 그리고 래칫을 **지적 범위 밖으로 넓히지 않는다**: `config_hash`
어긋남은 여전히 BLOCK 이고, allowlist 밖 스텝은 올림이어도 BLOCK 이다.

★「메우기는 호출 0」과 「어떤 재개든 호출 0」은 **다른 말**이다(Codex).
 기존 `done` 에 없는 대상은 여전히 `remaining` 으로 산다. 여기서 잠그는
 것은 **보강 때문에 호출이 늘지 않는다**는 것뿐이고, 그것은
 `test_entity_t2i_resume_buys_only_the_missing_ones.py` 가 잰다 — 그
 고정물은 **일부 완료 + 빠진 셋** 모양이라 「호출 3」을 센다.
★이번 화가 정말 **0** 인지(큐 전체가 `done` 인지)는 이 시험이 아니라
 **그 화의 실측**이다. 시험이 잰 것으로 적지 않는다.
"""
from __future__ import annotations

import pytest

from app.core.step_manifest import (_LEGACY_SCHEMA_BUMP_ALLOWLIST,
                                    get_manifest_dict)
from app.core.step_runner import ResumeAction, StepRunner


def _runner() -> StepRunner:
    r = StepRunner.__new__(StepRunner)
    r.step_id = "entity_t2i"
    r.project_config = {}
    return r


class TestTheBumpIsWiredToTheAllowlist:
    def test_the_manifest_bump_landed(self):
        """★올림이 **manifest 에** 실려야 재평가가 일어난다."""
        assert get_manifest_dict("entity_t2i")["schema_version"] >= 4

    def test_this_step_is_on_the_allowlist(self):
        assert "entity_t2i" in _LEGACY_SCHEMA_BUMP_ALLOWLIST

    def test_a_schema_bump_reruns_itself(self):
        d = _runner()._evaluate_contract_drift(
            "schema_version mismatch: cp=3 manifest=4")
        assert d.action == ResumeAction.RERUN_SELF, (
            f"★올림이 BLOCK 이면 빈 칸을 영영 못 메운다 — {d.action}")
        assert d.origin == "contract_drift"


class TestTheRatchetDoesNotWiden:
    """★이 올림이 **다른 어긋남까지** 자동 재실행으로 풀면 안 된다."""

    def test_a_config_hash_mismatch_still_blocks(self):
        d = _runner()._evaluate_contract_drift(
            "config_hash mismatch: cp=aaa manifest=bbb")
        assert d.action == ResumeAction.BLOCK

    def test_a_step_off_the_allowlist_still_blocks(self):
        r = _runner()
        r.step_id = "entity_detail"
        assert r.step_id not in _LEGACY_SCHEMA_BUMP_ALLOWLIST
        d = r._evaluate_contract_drift(
            "schema_version mismatch: cp=1 manifest=2")
        assert d.action == ResumeAction.BLOCK


class TestRerunSelfIsNotForce:
    """★★`RERUN_SELF` 가 force 처럼 굴면 **219개를 다시 사고 하류가 날아간다**."""

    def test_it_runs_in_resume_mode_and_destroys_nothing(self, monkeypatch):
        r = _runner()
        seen = {}

        def _boom(name):
            def _f(*a, **k):
                pytest.fail(f"★rerun_self 가 {name} 을 불렀다 — force 다")
            return _f

        monkeypatch.setattr(r, "cleanup_artifacts",
                            _boom("cleanup_artifacts"), raising=False)
        monkeypatch.setattr(r, "invalidate_downstream",
                            _boom("invalidate_downstream"), raising=False)
        monkeypatch.setattr(r, "clear_checkpoint",
                            _boom("clear_checkpoint"), raising=False)
        monkeypatch.setattr(
            r, "_execute_and_finalize",
            lambda **kw: seen.update(kw) or {"ok": True}, raising=False)

        r._execute_rerun_self()
        assert seen.get("execute_mode") == "resume", (
            f"★force 로 격상됐다 — {seen}")
