"""fixture 주행의 **3층 상한표**. ★유료 0.

Codex 2026-08-31 — 「스텝별 논리 + 모델별 물리」 두 줄로는 부족하다.
①logical plan(reused/cache/new 분리) ②counted transmission cap
③raw HTTP upper bound(key-slot·fallback·SDK retry). 그리고 **reserve 가 없는
유료 경로는 상한을 주장하지 말 것**.
"""
from __future__ import annotations

import pytest

from tools.grounding_audit import canary_cost_table as ct

PINNED = {"num_retries": 0, "enable_fallback": False}
LOOSE = {"num_retries": 3, "enable_fallback": True}


class TestThePaidListComesFromTheGraph:
    def test_it_is_derived_not_hand_written(self):
        """★손으로 적은 목록이 아니라 **의존 그래프**에서 나온다."""
        from app.core.step_manifest import STEP_MANIFEST as M

        steps = ct.steps_to("scene_detail")
        assert "scene_detail" in steps
        assert steps == sorted(steps, key=lambda s: M[s].get("order") or 0)

    def test_a_code_only_step_is_not_counted_as_paid(self):
        """★`provider='-'` 는 모델을 안 부른다 — 세면 상한이 헐거워진다."""
        from app.core.step_manifest import STEP_MANIFEST as M

        steps = set(ct.steps_to("scene_detail"))
        code_only = [s for s in steps
                     if str(M[s].get("provider") or "-").strip() in ("", "-")]
        assert code_only == [], f"★모델 없는 스텝이 섞였다: {code_only}"

    def test_a_shorter_target_needs_fewer_steps(self):
        assert len(ct.steps_to("episode_summary")) < \
            len(ct.steps_to("scene_detail"))


class TestTheThreeLayersAreSeparate:
    def _rows(self, contract=PINNED, cap=1):
        return ct.plan_rows(logical_caps={s: cap for s in ct.steps_to()},
                            contract=contract)

    def test_every_row_carries_all_three(self):
        for r in self._rows():
            for k in ("logical_cap", "logical_new", "logical_reused",
                      "logical_cache_hit", "counted_door", "counted_cap",
                      "key_slots", "raw_upper", "model_alias",
                      "model_physical", "contract"):
                assert k in r, f"★{r['step']} 에 {k} 가 없다"

    def test_the_text_path_names_its_central_doors(self):
        """★★앞 판은 「문이 없다」고 적었는데 **반대였다** (Codex BLOCK 1).

        물리 전송 자리에 이미 걸려 있고 팔을 안 들었을 뿐이다. 이 시험은
        지우지 않고 **뒤집었다** — 그때 무엇을 잘못 봤는지 남긴다
        ([[feedback-my-test-locked-the-thing-i-must-remove]]).
        """
        for r in self._rows():
            assert r["counted_door"] == ct.CENTRAL_DOORS
            assert r["counted_cap"] is not None, "★막을 수 있는 수를 안 적었다"

    def test_the_counted_and_raw_are_not_the_same_number(self):
        """★★잠근 계약에서 raw 는 counted 의 **3배**다 — SDK 재시도가 문 아래."""
        rows = self._rows()
        t = ct.totals(rows)
        assert t["raw_upper_total"] == t["counted_cap_total"] * 3, \
            f"★counted {t['counted_cap_total']} · raw {t['raw_upper_total']}"

    def test_the_shared_contract_is_the_source(self):
        """★식을 두 벌로 안 만든다 — `ref_canary` 와 같은 계약을 쓴다."""
        from tools.grounding_audit import call_bound_contract as cb

        got = ct.physical_upper(logical=7, contract=LOOSE, slots=2)
        assert got == cb.bounds(logical=7, contract=LOOSE, slots=2)

    def test_a_loose_contract_raises_the_raw_bound(self):
        """★재시도·fallback 을 켜면 물리 상한이 **커진다** — 표가 따라간다."""
        tight = ct.totals(self._rows(PINNED))["raw_upper_total"]
        loose = ct.totals(self._rows(LOOSE))["raw_upper_total"]
        assert loose > tight, f"★계약이 헐거워졌는데 상한이 그대로: {loose}"
        assert loose == tight * 3 * 4, "★tier 3 × (1+재시도 3) 이 아니다"

    def test_the_counted_bound_excludes_the_layers_below_the_door(self):
        """★★재시도는 `reserve` **뒤**라 counted 에 안 들어간다.

        production `physical_per_logical` 은 그것까지 곱하므로 헐거운 계약에서
        counted 보다 **크다** — 그 함수는 「막을 수 있는 수」가 아니다.
        """
        from app.core.steps.grounding_chunk_step import physical_per_logical

        got = ct.physical_upper(logical=5, contract=LOOSE, slots=2)
        assert got["counted"] < 5 * physical_per_logical(LOOSE, 2)
        assert got["counted"] == 5 * 3 * 2      # 논리 × tier × 슬롯

    def test_unknown_slots_are_not_invented(self):
        got = ct.physical_upper(logical=5, contract=PINNED, slots=None)
        assert got["counted"] is None and got["raw_http"] is None


class TestTheTotalsSayWhatIsNotMeasured:
    def test_nothing_is_unmeasured_once_the_doors_are_named(self):
        """★중앙 문 둘이 잡으므로 미측정은 **없다** — 다만 raw 는 못 막는다."""
        rows = ct.plan_rows(logical_caps={s: 1 for s in ct.steps_to()},
                            contract=PINNED)
        t = ct.totals(rows)
        assert t["unmeasured_steps"] == []
        assert "막을 수 없는" in t["★note"]

    def test_an_unknown_bound_makes_the_total_unknown(self):
        rows = [{"step": "x", "logical_cap": 1, "raw_upper": None,
                 "counted_cap": None, "counted_door": ct.UNMEASURED}]
        t = ct.totals(rows)
        assert t["raw_upper_total"] is None and t["counted_cap_total"] is None


class TestTheGateStopsBeforeTheNextStep:
    def test_going_over_stops(self):
        with pytest.raises(ct.CapExceeded):
            ct.assert_within(observed={"beat_extract": 4},
                             approved={"beat_extract": 2})

    def test_an_unapproved_step_stops(self):
        """★승인 목록에 없는 스텝이 돌면 그것도 **승인 밖**이다."""
        with pytest.raises(ct.CapExceeded):
            ct.assert_within(observed={"어떤_스텝": 1}, approved={})

    def test_staying_within_passes(self):
        ct.assert_within(observed={"beat_extract": 2},
                         approved={"beat_extract": 2, "shot_extract": 9})

    def test_the_message_names_both_numbers(self):
        with pytest.raises(ct.CapExceeded) as e:
            ct.assert_within(observed={"a": 7}, approved={"a": 3})
        assert "7" in str(e.value) and "3" in str(e.value)


# ─────────────────────────────────────────────────────────────────────
# ★★★「42개」는 실행계획이 아니다 (Codex BLOCK 3, 2026-08-31)
# ─────────────────────────────────────────────────────────────────────

# ★한 벌만 둔다 — 두 곳에 적으면 한쪽만 고쳐진다. 실제로 그랬다:
#  `canary_run` 쪽에서 못 정하는 값을 빼고도 여기 사본이 남아 있었다.
from tools.grounding_audit.canary_run import default_fixture_config  # noqa: E402
FIXTURE_CONFIG = default_fixture_config()


class TestThePlanIsNotTheConservativeList:
    def test_the_applied_list_is_smaller(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert len(p["applied"]) < p["closure_total"]
        # ★`new` 는 **유료로 셀 것**만이다 — 실행 목록 전부가 아니다
        assert p["new"] == len(p["applied_metered"])

    def test_the_conditional_grounding_steps_are_skipped(self):
        """★★D 가 inert 라 `if_grounding_v2` 셋은 **안 돈다**."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        skipped = {r["step"] for r in p["skipped"]}
        assert {"grounding_a0", "grounding_plan",
                "grounding_research"} <= skipped

    def test_turning_it_on_brings_them_back(self):
        """★설정이 바뀌면 목록이 바뀐다 — 표가 따라가는지 본다."""
        on = ct.execution_plan(config={**FIXTURE_CONFIG,
                                       "grounding_v2": True})
        assert {"grounding_a0", "grounding_plan", "grounding_research"} <= \
            {r["step"] for r in on["applied"]}

    def test_an_undeclared_rule_is_unknown_not_applied(self):
        """★★모르는 것을 **「적용된다」로 접지 않는다**."""
        p = ct.execution_plan(config={})
        assert p["unknown"], "★모르는 것이 하나도 없다고 나왔다"
        assert all(r["applicability"] != "always" for r in p["unknown"])

    def test_the_note_forbids_using_the_total_as_a_denominator(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert "분모도 아니다" in p["★means"]

    def test_reuse_is_declared_not_measured(self):
        """★첫 판의 0 은 **선언**이다 — 관측으로 적지 않는다."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert p["reused"] == 0 and p["cache_hit"] == 0
        assert "실측" in p["★reuse_note"]


class TestTheBoundaryComesFromTheRealModule:
    def test_a_step_resolves_to_its_module(self):
        assert ct.step_module("background_prompt", ).endswith(
            "background_prompt_step.py")

    def test_an_unknown_step_is_not_guessed(self):
        assert ct.step_module("없는_스텝") is None

    def test_no_applied_step_uses_the_doorless_path(self):
        """★문 **없는** 경로를 지나는 스텝은 계획에 없다."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert [r for r in p["applied"] if r["counted"] is False] == []

    def test_an_unknown_door_blocks_too(self, monkeypatch):
        """★★**모르는 것도 막는다** — 앞 판은 「못 찾았다」를 통과시켰다.

        Codex 2026-08-31: 깊이에서 못 찾은 것을 `counted=True` 로 접던 자리.

        ★지금 실제 계획에는 모르는 스텝이 **없다**(전부 metered 아니면 free).
        그래서 실제 데이터에 기대지 않고 **모르는 판을 하나 만들어** 잰다 —
        데이터가 마침 그래서 통과하는 시험은 계약을 안 지킨다.
        """
        real = ct.boundary_of

        def _one_unknown(step, **kw):
            if step == "entity_merge":
                return {"boundary": None, "module": "x", "counted": None,
                        "why": "일부러 모른다고 했다"}
            return real(step, **kw)

        monkeypatch.setattr(ct, "boundary_of", _one_unknown)
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert p["applied_unknown"] == ["entity_merge"]
        assert "entity_merge" in {r["step"]
                                  for r in p["blocked_no_or_unknown_door"]}
        assert "돌리지 않는다" in p["★means"]

    def test_a_passed_function_counts_as_using_it(self):
        """★★`fn=call_structured` 로 **넘기는** 것도 부르는 것이다.

        글자로 `이름(` 만 찾던 앞 판은 이것을 놓쳤다 (실측:
        `background_classify_step.py:135`).
        """
        got = ct.boundary_of("background_classify")
        assert got["counted"] is True, got["why"]

    def test_a_deterministic_step_is_marked_free_not_unknown(self):
        """★모델을 부르는 이름이 **하나도 없으면** 유료가 아니다.

        앞 판은 이것을 「모른다」로 두고 막았다 — 그러면 production 실행에서
        빠져 실제와 달라진다 (Codex 2026-08-31).
        """
        got = ct.boundary_of("shot_dependency")
        assert got["counted"] == ct.NO_LLM
        assert "하나도 없다" in got["why"]
        assert "드러난다" in got["why"], "★주행이 확인한다는 것을 안 적었다"


class TestTheDeclaredRuleMeansWhatProductionMeans:
    """★선언 평가기가 production `resolve_applicability` 와 뜻이 같아야 한다."""

    def _one(self, rule, cfg):
        plan = ct.execution_plan(config=cfg)
        # 규칙을 직접 물어보려고 최소 행을 만든다
        from app.core.step_manifest import STEP_MANIFEST as M

        for s in ct.steps_to():
            if M[s].get("applicability") == rule:
                for bucket in ("applied", "skipped", "unknown"):
                    if any(r["step"] == s for r in plan[bucket]):
                        return bucket
        return None

    def test_always_is_applied(self):
        assert self._one("always", FIXTURE_CONFIG) == "applied"

    def test_an_if_rule_follows_the_config(self):
        assert self._one("if_grounding_v2", FIXTURE_CONFIG) == "skipped"
        assert self._one("if_grounding_v2",
                         {**FIXTURE_CONFIG, "grounding_v2": True}) == "applied"

    def test_an_unknown_rule_shape_is_not_folded_to_true(self):
        """★`if_` 도 아닌 모르는 규칙을 **참으로 안 접는다**."""
        got = ct.execution_plan(
            config=FIXTURE_CONFIG,
            resolve=lambda s, row, cfg: None if s == "entity_merge" else True)
        assert any(r["step"] == "entity_merge" for r in got["unknown"])


class TestThereIsPaidWorkOutsideTheStepList:
    """★★manifest 에 **없는** 유료가 있다 — fixture 를 세우는 길에서 찾았다."""

    def test_creating_a_project_costs_one_call(self):
        rows = ct.bootstrap_rows(contract=PINNED)
        assert rows and rows[0]["logical"] == 1
        assert rows[0]["model_alias"] == "gpt"

    def test_it_is_counted_with_the_same_layers(self):
        rows = ct.bootstrap_rows(contract=PINNED)
        one = ct.physical_upper(logical=1, contract=PINNED,
                                slots=rows[0]["key_slots"])
        assert rows[0]["counted_cap"] == one["counted"]
        assert rows[0]["raw_upper"] == one["raw_http"]

    def test_the_named_call_site_still_exists(self):
        """★★파일:줄을 적어 뒀으면 **그것이 아직 있는지** 본다."""
        import ast
        import inspect

        from app.services import project_service as ps

        tree = ast.parse(inspect.getsource(ps.generate_english_name))
        called = {n.func.id for n in ast.walk(tree)
                  if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)}
        assert "call_text" in called, "★유료 호출이 사라졌거나 이름이 바뀌었다"

    def test_making_an_episode_is_free(self):
        """★PDF 에서 글을 뽑는 것은 코드다 — 모델을 안 부른다."""
        import ast
        import inspect

        from app.modules import pdf_parser

        tree = ast.parse(inspect.getsource(pdf_parser.extract_text_from_pdf))
        names = {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
        assert not (names & {"call_text", "call_structured",
                             "llm_completion", "router_completion"})


class TestAppliedIsNotTheSameAsPaid:
    """★★Codex 2026-08-31 — `applied != paid`. 셋으로 가른다."""

    def test_the_three_buckets_add_up(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert (len(p["applied_free"]) + len(p["applied_metered"])
                + len(p["applied_unknown"])) == len(p["applied"])

    def test_the_deterministic_steps_are_free_not_blocked(self):
        """★★모델을 아예 안 부르는 셋 — 빼지 않고 **그대로 돌린다**."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert {"shot_dependency", "floor_plan_overlay_payload",
                "episode_reference_policy"} <= set(p["applied_free"])
        assert p["applied_unknown"] == []
        assert p["blocked_no_or_unknown_door"] == []

    def test_nothing_is_dropped_from_the_run(self):
        """★production 과 달라지면 안 된다 — 뺀 스텝이 없어야 한다.

        ★★앞에는 여기에 **39** 를 박아 뒀다. 그 수는 내가 fixture 설정에
        `visual_continuity_anchor_enabled: False` 라고 적어 둔 데서 나온 것인데,
        그 규칙은 `settings` 를 보고 `.env` 는 켜져 있었다 — 즉 시험이 **내
        틀린 믿음을 숫자로 굳혀** 두고 있었다. 수 대신 **불변식**으로 잰다.
        """
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert len(p["applied"]) + len(p["skipped"]) == p["closure_total"]
        # ★코드 스텝은 유료가 아니어도 **돈다** — 뒤가 그 기록을 본다
        assert "scene_save" in [r["step"] for r in p["applied"]]
        # ★건너뛴 것은 **production 술어나 선언이 아니라고 답한 것**뿐이다
        from app.core.step_manifest import STEP_MANIFEST as M

        for r in p["skipped"]:
            rule = str(M[r["step"]].get("applicability") or "always")
            live = ct._production_says(rule)
            said = live if live is not None else FIXTURE_CONFIG.get(rule[3:])
            assert said is False, f"{r['step']} 은 왜 빠졌나"

    def test_a_step_that_does_call_a_model_is_not_free(self):
        """★양성 대조 — 실제로 부르는 스텝은 `free` 로 안 간다."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert "scene_detail" in p["applied_metered"]
        assert "scene_detail" not in p["applied_free"]


class TestTheFreeVerdictIsLockedByTheRun:
    def test_a_free_step_that_sends_stops(self):
        with pytest.raises(ct.CapExceeded) as e:
            ct.assert_free_step_sent_nothing(
                "shot_dependency", {"counted": 2, "denied": 0})
        assert "분류가 틀렸다" in str(e.value)

    def test_a_free_step_that_sends_nothing_passes(self):
        ct.assert_free_step_sent_nothing("shot_dependency",
                                         {"counted": 0, "denied": 0})

    def test_the_delta_is_the_difference(self):
        got = ct.per_step_delta({"used": 3, "denied": 1},
                                {"used": 7, "denied": 2})
        assert got == {"counted": 4, "denied": 1}


class TestTheExecutionListIsNotTheCostList:
    """★★Codex 실측 (2026-08-31): closure 43 · 비용 목록 42.

    빠진 하나가 `scene_save`(order 6 · always · provider '-')다. 비용을 세려고
    거른 목록을 **실행에 쓰면** 뒤 스텝이 제 체크포인트 없이 시작한다.
    """

    def test_the_closure_is_bigger_than_the_cost_list(self):
        assert len(ct.execution_steps_to()) > len(ct.steps_to())

    def test_the_code_step_is_in_the_closure_but_not_the_cost_list(self):
        assert "scene_save" in ct.execution_steps_to()
        assert "scene_save" not in ct.steps_to()
        assert ct.is_code_step("scene_save") is True

    def test_the_execution_plan_keeps_the_code_step(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert "scene_save" in [r["step"] for r in p["applied"]]
        assert "scene_save" in p["applied_free"]

    def test_the_code_step_comes_before_its_consumers(self):
        """★`scene_save` 가 뒤 소비자보다 **먼저** 돌아야 한다."""
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        order = [r["step"] for r in p["applied"]]
        i = order.index("scene_save")
        for later in ("scene_summary", "beat_extract", "scene_detail"):
            assert order.index(later) > i, f"★{later} 가 먼저다"

    def test_the_note_says_applied_is_the_run_list(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert "돌 스텝" in p["★means"]
        assert "실행 목록도 분모도 아니다" in p["★means"]

    def test_new_counts_only_the_metered_ones(self):
        p = ct.execution_plan(config=FIXTURE_CONFIG)
        assert p["new"] == len(p["applied_metered"]) < len(p["applied"])


def _yes(runner=None):
    """★module 수준의 **이름 있는 함수**로 둔다.

    앞에는 `lambda r: True` 를 썼는데, `inspect.getsource` 가 그 lambda 를
    감싼 **한 줄 전체**(들여쓰기 포함)를 돌려줘 `ast.parse` 가 못 읽었다.
    그러면 `_needs_a_runner` 가 「모른다」쪽으로 접혀 시험이 다른 것을 잰다.
    """
    from app.core.config import settings          # ★진짜와 같은 모양으로

    del settings
    return True


def _no(runner=None):
    from app.core.config import settings

    del settings
    return False


class TestItAsksProductionNotMyOwnDict:
    """★★★계획표가 **제가 지은 dict** 로 적용 여부를 풀던 것 (실측 2026-09-01).

    `if_visual_continuity_anchor_enabled` 는 **`settings`** 를 보는데, fixture
    선언에는 `False` 라고 적혀 있었다. `.env` 에는 `true` 였다. 그래서 계획표는
    「적용 안 됨·0원」이라 적고 pipeline 은 **실제로 사러 갔다**. 판이 선 것은
    게이트 덕분이지, 어긋난 것은 pipeline 이 아니라 **재는 쪽**이었다.
    """

    def test_it_calls_the_production_predicate(self, monkeypatch):
        from app.core import applicability as ap

        monkeypatch.setitem(ap.APPLICABILITY_VALIDATORS,
                            "if_background_mode", _yes)
        assert ct._production_says("if_background_mode") is True
        monkeypatch.setitem(ap.APPLICABILITY_VALIDATORS,
                            "if_background_mode", _no)
        assert ct._production_says("if_background_mode") is False

    def test_a_predicate_that_needs_a_runner_is_undecided(self):
        """★없는 runner 를 흉내 내지 않는다 — 흉내가 곧 지어낸 답이다."""
        assert ct._production_says("if_grounding_v2") is None
        assert ct._production_says("if_no_such_rule") is None

    def test_a_declared_value_that_disagrees_stops_everything(self,
                                                              monkeypatch):
        from app.core import applicability as ap

        monkeypatch.setitem(ap.APPLICABILITY_VALIDATORS,
                            "if_background_mode", _yes)
        with pytest.raises(ct.ApplicabilityContradiction, match="거짓말"):
            ct.execution_plan(config={"grounding_v2": False,
                                      "background_mode": False})

    def test_production_wins_when_nothing_was_declared(self, monkeypatch):
        from app.core import applicability as ap

        monkeypatch.setitem(ap.APPLICABILITY_VALIDATORS,
                            "if_background_mode", _no)
        got = ct.execution_plan(config={"grounding_v2": False})
        names = [r["step"] for r in got["skipped"]]
        assert "background_classify" in names
        assert "floor_plan_render" in names

    def test_the_declared_value_still_answers_when_production_cannot(self):
        """★runner 가 필요한 규칙은 **선언**으로 푼다 — 그것뿐이라서."""
        got = ct.execution_plan(config={"grounding_v2": False,
                                        "background_mode": True})
        assert [r["step"] for r in got["skipped"]] == [
            "grounding_a0", "grounding_plan", "grounding_research"]
        assert not got["applied_unknown"]


class TestAPredicateThatSwallowsItsOwnErrorMustNotAnswer:
    """★★★「모른다」가 **「아니다」로 둔갑**하던 것 (실측 2026-09-01).

    `_if_has_outlooks` · `_if_planning_doc` 은 runner 를 보는데, 속으로
    `except` 를 잡고 `False` 를 돌려준다. 예외로 가리려던 앞 판은 그 `False`
    를 **답으로 받아** 그 스텝을 「건너뜀·0원」으로 계획했다. 이제 **AST 로**
    runner 를 보는지 먼저 가린다.
    """

    def test_a_predicate_that_reads_the_runner_never_answers(self):
        from app.core.applicability import APPLICABILITY_VALIDATORS as V

        for name, fn in V.items():
            if ct._needs_a_runner(fn):
                assert ct._production_says(name) is None, name

    def test_the_two_that_swallow_their_error_are_caught(self):
        """★양성 대조 — 실제로 `False` 를 돌려주던 둘을 이름으로 확인한다."""
        from app.core.applicability import APPLICABILITY_VALIDATORS as V

        for name in ("if_has_outlooks", "if_planning_doc"):
            fn = V[name]
            assert ct._needs_a_runner(fn) is True
            assert fn(None) is False        # ★속으로 삼키고 False 를 준다
            assert ct._production_says(name) is None   # ★그래도 안 받는다

    def test_a_settings_only_predicate_still_answers(self):
        """★음성 대조 — runner 를 안 보는 것은 그대로 답해야 한다."""
        from app.core.applicability import APPLICABILITY_VALIDATORS as V

        assert ct._needs_a_runner(V["if_background_mode"]) is False
        assert ct._production_says("if_background_mode") is not None

    def test_an_unreadable_predicate_is_treated_as_unknown(self):
        """★소스를 못 읽으면 **모른다** 쪽으로 접는다."""
        assert ct._needs_a_runner(len) is True
