"""요청 계약을 **진짜로** 잠근다. ★유료 0.

Codex BLOCK (2026-08-31) —

> `PINNED` 은 실제 요청 계약이 아니라 **계산용 dict** 뿐입니다. Router 는
> `settings.llm_max_retries`(기본 3)로 생성되고, `canary_text_scope` 는
> Router 진입 **전** counted 만 세므로 **Router 내부 재시도는 안 보입니다.**
> 되돌리면 provider 전 시험이 **반드시 실패**해야 합니다.
"""
from __future__ import annotations

import pytest

from tools.grounding_audit import canary_request_lock as rl


@pytest.fixture
def fresh():
    """Router 가 **아직 없는** 판을 만든다.

    ★끝나고도 **비운다.** `monkeypatch` 는 setup 때 값을 붙잡아 그것으로
    되돌리는데, 앞 시험이 남긴 가짜 Router 를 붙잡으면 그것이 그대로 살아나
    다음 시험이 순서에 흔들린다 (실측 2026-08-31).
    """
    from app.modules.llm import llm_client as lc

    def _clear():
        lc._router = None
        lc._binding = None      # ★`_binding` 이 정본이다 — 이것도 비운다

    _clear()
    yield lc
    _clear()


@pytest.fixture
def retries_on(monkeypatch):
    """★★재시도를 **켠다**. 시험 conftest 는 0 으로 두는데, production 은
    켜져 있다 — 0 으로 재면 문이 잡은 것인지 설정 덕인지 못 가른다."""
    from app.core.config import settings

    monkeypatch.setattr(settings, "llm_max_retries", 3)
    assert settings.llm_max_retries == 3


class TestItReallyChangesTheValue:
    def test_inside_the_lock_the_setting_is_zero(self, fresh, retries_on):
        from app.core.config import settings

        was = settings.llm_max_retries
        with rl.canary_request_lock() as got:
            assert settings.llm_max_retries == 0
            assert got["was"] == was
        assert settings.llm_max_retries == was, "★안 되돌렸다"

    def test_it_restores_even_on_error(self, fresh):
        from app.core.config import settings

        was = settings.llm_max_retries
        with pytest.raises(RuntimeError):
            with rl.canary_request_lock():
                raise RuntimeError("일부러")
        assert settings.llm_max_retries == was

    def test_the_router_is_built_with_zero(self, fresh, retries_on,
                                           monkeypatch):
        """★★문구가 아니라 **지어진 객체**에서 읽는다."""
        from app.modules.llm.llm_client import _get_router_binding

        monkeypatch.setenv("GEMINI_API_KEY", "가짜")
        with rl.canary_request_lock():
            _get_router_binding()
            got = rl.assert_router_locked()
        assert got["num_retries"] == 0 and got["locked"] is True


class TestPuttingItBackMustFail:
    """★★★되돌리면 **provider 전에** 반드시 실패한다 (Codex 요구)."""

    def test_a_router_with_retries_is_refused(self, fresh):
        """★잠그지 **않고** 지어진 Router(재시도 3)는 거절된다.

        ★Router 를 **직접 세워 둔다** — 실제로 짓게 하면 앞 시험이 남긴
        것에 따라 결과가 갈린다(순서 의존). 재는 것은 `assert_router_locked`
        가 **읽은 값으로 판정하는가** 하나다.
        """
        from app.modules.llm import llm_client as lc

        class _R:
            num_retries = 3

        lc._binding = lc._RouterBinding(slot="s", router=_R())
        with pytest.raises(rl.RequestContractRefused) as e:
            rl.assert_router_locked()
        assert "num_retries" in str(e.value) and "3" in str(e.value)

    def test_an_already_built_router_cannot_be_locked(self, fresh):
        """★이미 지어졌으면 못 바꾼다 — **새 프로세스**여야 한다."""
        from app.modules.llm import llm_client as lc

        class _R:
            num_retries = 3

        lc._binding = lc._RouterBinding(slot="s", router=_R())
        with pytest.raises(rl.RequestContractRefused) as e:
            with rl.canary_request_lock():
                pass
        assert "새 프로세스" in str(e.value)

    def test_an_unreadable_router_stops_the_run(self, fresh):
        """★★못 읽으면 **못 산다** — 「모른다」로 지나가면 안 된다.

        앞 판은 `locked=None` 을 내고도 주행을 안 막았다 (Codex 2026-08-31).
        """
        from app.modules.llm import llm_client as lc

        class _R:
            pass

        lc._binding = lc._RouterBinding(slot="s", router=_R())
        with pytest.raises(rl.RequestContractRefused) as e:
            rl.assert_router_locked()
        assert "못 읽었다" in str(e.value)
        # ★느슨하게 물으면 「모른다」로 답한다 — 감사 기록용이다
        assert rl.assert_router_locked(strict=False)["locked"] is None

    def test_no_router_at_all_stops(self, fresh):
        with pytest.raises(rl.RequestContractRefused) as e:
            rl.assert_router_locked()
        assert "아직 없다" in str(e.value)


class TestTheRunUsesIt:
    def test_the_pipeline_stage_is_wrapped(self):
        """★AST — pipeline 이 **잠근 채** 돈다."""
        import ast
        import inspect

        from tools.grounding_audit import canary_run as cr

        tree = ast.parse(inspect.getsource(cr.run).lstrip())
        names = [n.func.attr for n in ast.walk(tree)
                 if isinstance(n, ast.Call)
                 and isinstance(n.func, ast.Attribute)]
        assert "canary_request_lock" in names
        assert "assert_router_locked" in names

    def test_the_contract_now_pins_all_three_layers(self):
        """★2026-09-02 뒤집었다 — 이제 **셋을 다** 잠근다 (Codex).

        앞 판은 「`enable_fallback` 은 안 잠근다」였고 그것이 정직했다.
        그런데 SDK 재시도를 실제로 0 으로 박을 수 있다는 것이 무료 probe 로
        확인됐고(기본 2 · 준 값 0), tier 를 열어 두면 상한 70 에서 조기
        정지해 판이 inconclusive 로 끝난다. 그래서 셋을 다 잠근다.
        ★적기만 하면 안 된다 — `canary_request_lock` 이 실제로 건다.
        """
        from tools.grounding_audit import canary_run as cr

        assert cr.LOCKED_CONTRACT["num_retries"] == 0
        assert cr.LOCKED_CONTRACT["enable_fallback"] is False
        assert cr.LOCKED_CONTRACT["sdk_max_retries"] == 0

    def test_what_the_contract_says_is_what_the_lock_does(self):
        """★★★적힌 값과 **실제로 거는 값**이 같아야 한다.

        계약 dict 만 고치고 잠금을 안 고치면 「계산용 dict」로 되돌아간다 —
        Codex 가 앞서 BLOCK 한 바로 그 자리다.
        """
        from tools.grounding_audit import canary_request_lock as rl
        from tools.grounding_audit import canary_run as cr

        assert cr.LOCKED_CONTRACT["num_retries"] == rl.CANARY_NUM_RETRIES
        assert (cr.LOCKED_CONTRACT["sdk_max_retries"]
                == rl.CANARY_SDK_MAX_RETRIES)
        assert (cr.LOCKED_CONTRACT["enable_fallback"]
                is rl.CANARY_ENABLE_FALLBACK)


class TestTheBootstrapIsInsideTheLockToo:
    """★★부트스트랩의 유료 1회도 문 **아래**가 잠긴 채 나가야 한다.

    그리고 부트스트랩이 `create_project` → `generate_english_name` 으로
    **Router 를 짓는다** — 그 뒤에 잠그려 하면 「이미 지어졌다」로 선다.
    그래서 하나의 잠금이 둘을 **같이** 감싸야 한다.
    """

    def test_the_lock_opens_before_the_bootstrap(self):
        import ast
        import inspect

        from tools.grounding_audit import canary_run as cr

        tree = ast.parse(inspect.getsource(cr.run).lstrip())
        seq = sorted(((n.lineno,
                       n.func.attr if isinstance(n.func, ast.Attribute)
                       else getattr(n.func, "id", ""))
                      for n in ast.walk(tree) if isinstance(n, ast.Call)))
        names = [x[1] for x in seq]
        assert names.index("canary_request_lock") < names.index("bootstrap")
        assert names.index("bootstrap") < names.index("run_pipeline")

    def test_it_checks_the_router_after_the_bootstrap(self):
        """★부트스트랩이 지은 Router 도 0인지 **거기서** 본다."""
        import inspect

        from tools.grounding_audit import canary_run as cr

        src = inspect.getsource(cr.run)
        assert "router_lock_after_bootstrap" in src

    def test_building_a_router_inside_the_lock_stays_zero(self, fresh,
                                                          retries_on,
                                                          monkeypatch):
        """★잠금 안에서 지으면 0 이고, 그 뒤 pipeline 도 같은 Router 를 쓴다."""
        from app.modules.llm.llm_client import _get_router_binding

        monkeypatch.setenv("GEMINI_API_KEY", "가짜")
        with rl.canary_request_lock():
            assert rl.prepare_router()["num_retries"] == 0   # ★첫 구매 전
            _get_router_binding()                # ★부트스트랩이 하는 일
            assert rl.assert_router_locked()["num_retries"] == 0
            _get_router_binding()                # ★pipeline 이 다시 쓴다
            assert rl.assert_router_locked()["num_retries"] == 0


class TestADeniedCallDoesNotEatTheApproval:
    """★★★안쪽 예산이 막아 **한 번도 안 나간** 호출이 run 전체 승인선을
    먹던 것 (2026-09-02 실측).

    재판정 판에서 `entity_t2i` 의 거절 16건이 남은 23을 다 써서, 정작
    하려던 재판정이 **시작도 못 했다**. Opik 성공 0 · 텍스트 예산 `used 0`
    이 「한 푼도 안 나갔다」를 증언한다. 돈이 안 나간 것에 상한을 쓰면 안
    된다.
    """

    def test_a_denied_reservation_does_not_move_the_counter(self,
                                                             monkeypatch):
        """★★공개 끝점으로 잰다 — 잠금을 실제로 걸고, 안쪽이 막게 한다."""
        import pytest

        from app.core import research_call_budget as rcb
        from tools.grounding_audit import canary_request_lock as rl

        class _InnerRefused(RuntimeError):
            pass

        allow = {"ok": False}

        def _inner(*, source: str):
            if not allow["ok"]:
                raise _InnerRefused(f"안쪽 상한 — {source}")
            return None

        monkeypatch.setattr(rcb, "reserve_current_research_call", _inner)
        monkeypatch.setattr(rl, "router_already_built", lambda: False)
        with rl.canary_request_lock(transport_cap=3) as locked:
            c = locked["transport"]
            for _ in range(5):
                with pytest.raises(_InnerRefused):
                    rcb.reserve_current_research_call(source="x")
            assert c.used == 0, (
                f"★안 나간 호출 5건이 상한을 {c.used} 먹었다")
            allow["ok"] = True
            rcb.reserve_current_research_call(source="x")
            assert c.used == 1, "★지나간 것은 세야 한다"

    def test_the_gate_is_still_before_the_network(self):
        """★순서를 바꿔도 문은 **네트워크 앞**이다 — 상한 초과는 여전히 선다."""
        import pytest

        from tools.grounding_audit import canary_request_lock as rl

        c = rl.TransportCounter(cap=2, name="t")
        c.note("a")
        c.note("a")
        with pytest.raises(rl.TransportBudgetExceeded):
            c.note("a")
        assert c.used == 2 and c.denied == 1
