"""★끝난 스텝이 지문 어긋남으로 force 되면 dry 의 남은 표·상한 문·단위 확인에서 **산다**로 친다.

실측 4398a55dc0bb 재개(2026-09-03): 야외 스텝이 completed 인 채 force 였는데 dry 가 그것을 done 으로 쳐서
남은 표에 없고(정상 예상 합이 그만큼 작게 나왔다) 그룹 상한 문도 「살 자리에 없다」고 했다."""
from __future__ import annotations

import json

from tools.grounding_audit import canary_pipeline as cp
from tools.grounding_audit import canary_run as cr


def _cp(root, step, status="completed"):
    d = root / "projects" / "p" / "checkpoints" / "episodes" / "e" / step
    d.mkdir(parents=True, exist_ok=True)
    (d / "manifest.json").write_text(json.dumps({"status": status, "config_hash": "old"}), encoding="utf-8")


def test_a_completed_step_whose_hash_drifts_is_reported_as_forced(tmp_path, monkeypatch):
    monkeypatch.setattr(cr.ci, "root_dir", lambda rid: tmp_path)
    _cp(tmp_path, "outdoor_structure_form_reference"); _cp(tmp_path, "outlook_phase3"); _cp(tmp_path, "grounding_a0", "not_applicable")
    monkeypatch.setattr(cr, "_owner_requirement", lambda fx: {})
    monkeypatch.setattr(cp, "code_transition_covers_head", lambda root: True)
    monkeypatch.setattr(cp, "durable_status", lambda pid, epi, s: "not_applicable" if s == "grounding_a0" else "completed")
    monkeypatch.setattr(cp, "contract_drift_of",
                        lambda s, pid, epi, cfg: "config_hash mismatch" if s == "outdoor_structure_form_reference" else None)
    got = cr.forced_for_drift_of("r", ["outdoor_structure_form_reference", "outlook_phase3", "grounding_a0"], {"fixture": "x"})
    assert got == ["outdoor_structure_form_reference"]


def test_without_an_approved_transition_a_drifted_step_is_not_forced(tmp_path, monkeypatch):
    monkeypatch.setattr(cr.ci, "root_dir", lambda rid: tmp_path)
    _cp(tmp_path, "outdoor_structure_form_reference")
    monkeypatch.setattr(cr, "_owner_requirement", lambda fx: {})
    monkeypatch.setattr(cp, "code_transition_covers_head", lambda root: False)
    monkeypatch.setattr(cp, "durable_status", lambda pid, epi, s: "completed")
    monkeypatch.setattr(cp, "contract_drift_of", lambda s, pid, epi, cfg: "config_hash mismatch")
    assert cr.forced_for_drift_of("r", ["outdoor_structure_form_reference"], {"fixture": "x"}) == []


def test_the_dry_plan_reads_the_forced_list_before_the_done_set_is_used():
    import inspect
    src = inspect.getsource(cr.run)
    # ★2026-09-03: force/ack 를 가르는 `drift_reentry_of` 로 바뀌었다 — 관계(forced 목록이 unverified 셈보다 앞)는 그대로
    assert src.index('got["forced_for_drift"] = _dr["forced"]') < src.index("_unv = [x for x in built")



class TestADebtIsReopenedWithANormalCap:
    def test_plan_reentry_gives_a_completed_step_with_debt_a_normal_cap(self):
        got = cp.plan_reentry("completed", None, True, False, debt="다시 사야 할 대상 9개")
        assert got == {"mode": "resume", "cap_zero": False, "stop_why": None}
        assert cp.plan_reentry("completed", None, True, False, debt=None)["cap_zero"] is True

    def test_the_dry_lists_a_completed_step_whose_runner_says_debt(self, tmp_path, monkeypatch):
        monkeypatch.setattr(cr.ci, "root_dir", lambda rid: tmp_path)
        _cp(tmp_path, "reference_acquisition"); _cp(tmp_path, "outlook_phase3")
        monkeypatch.setattr(cr, "_owner_requirement", lambda fx: {})
        monkeypatch.setattr(cp, "durable_status", lambda pid, epi, s: "completed")
        monkeypatch.setattr(cp, "contract_drift_of", lambda s, pid, epi, cfg: None)
        monkeypatch.setattr(cp, "completion_debt_of",
                            lambda s, pid, epi, cfg: "다시 사야 할 대상 9개" if s == "reference_acquisition" else None)
        got = cr.reopened_for_debt_of("r", ["reference_acquisition", "outlook_phase3"], {"fixture": "x"})
        assert got == ["reference_acquisition"]

    def test_a_drifted_step_is_counted_once_as_forced_not_as_debt(self, tmp_path, monkeypatch):
        monkeypatch.setattr(cr.ci, "root_dir", lambda rid: tmp_path)
        _cp(tmp_path, "outdoor_structure_form_reference")
        monkeypatch.setattr(cr, "_owner_requirement", lambda fx: {})
        monkeypatch.setattr(cp, "durable_status", lambda pid, epi, s: "completed")
        monkeypatch.setattr(cp, "contract_drift_of", lambda s, pid, epi, cfg: "config_hash mismatch")
        monkeypatch.setattr(cp, "completion_debt_of", lambda s, pid, epi, cfg: "빚")
        assert cr.reopened_for_debt_of("r", ["outdoor_structure_form_reference"], {"fixture": "x"}) == []



class TestTheOutboundDoorsFollowACodeReopen:
    def test_a_completed_central_step_reopened_for_debt_gets_the_table_doors(self, monkeypatch):
        """★뒤집음 (Codex 2026-09-03 07:20): 문은 「중앙이 끝났나」가 아니라 **남은 outbound 소비자**로 연다 — 소비자 전부 끝나야 0."""
        from app.modules.pipeline.grounding_outbound_consumers import OUTBOUND_CONSUMER_STEPS
        monkeypatch.setattr(cr, "completed_steps_of", lambda rid, statuses=("completed",): list(OUTBOUND_CONSUMER_STEPS))
        table = {"counted": 480, "search": 120, "download": 240}
        assert cr.outbound_doors_on_resume("r", table, reopen=())["search"] == 0
        assert cr.outbound_doors_on_resume("r", table, reopen=[cr.CENTRAL_STEP])["search"] == 120

    def test_run_recomputes_the_doors_after_the_reopened_lists(self):
        import inspect
        src = inspect.getsource(cr.run)
        assert src.index('got["reopened_for_debt"] = ') < src.index("approved = outbound_doors_on_resume(rid, approved_table, reopen=sorted(_reopened_all), applied=_applied_ids)")
        assert src.index("approved = outbound_doors_on_resume(rid, approved_table, reopen=sorted(_reopened_all), applied=_applied_ids)") < src.index("cp.run_pipeline(")
