"""이미지 문이 **정말 잠기는지**. ★글 예산은 이미지를 못 센다.

실측 2026-09-01 — `scene_detail` 까지의 closure 안에 `floor_plan_render` 가
있고 그것은 `gpt-image-2` 를 산다. 글 예산(`canary_text_scope`)이 걸린 문 셋을
하나도 안 지나므로, 글 장부에는 **0 으로 적히고 실제로는 돈이 나간다**.
"""
from __future__ import annotations

import threading
from concurrent.futures import ThreadPoolExecutor

import pytest

from app.core import image_call_budget as ib
from tools.grounding_audit.canary_image_budget import (canary_image_scope,
                                                       image_delta,
                                                       image_doors, resolve)


class TestTheGateIsOpenUntilSomeoneInstallsABudget:
    def test_without_a_budget_the_door_is_a_no_op(self):
        """★「문이 있다」와 「문이 잠겼다」는 **다르다**.

        이것이 이 판의 결함이었다 — 문은 있는데 canary 가 안 깔아서
        `floor_plan_render` 가 그냥 지나갈 참이었다.
        """
        assert ib.get_current_budget() is None
        ib.reserve_current_call(source="test.no_budget")   # ★안 막힌다

    def test_with_a_zero_budget_the_door_refuses(self):
        with canary_image_scope(cap=0) as budget:
            with pytest.raises(ib.ImageCallBudgetExceeded):
                ib.reserve_current_call(source="test.zero")
            assert budget.snapshot()["denied"] == 1


class TestEveryThreadSeesIt:
    """★팬아웃마다 구멍이 나면 안 된다."""

    def test_a_worker_that_never_binds_is_still_gated(self):
        """★★`bind_current_budget` 을 **안 부르는** 팬아웃이 진짜 위험이다.

        production 의 모든 이미지 팬아웃이 그것을 부른다고 **믿지 않는다** —
        믿음이 아니라 문으로 막는다.
        """
        got = []

        def worker():
            try:
                ib.reserve_current_call(source="test.worker")
                got.append("샀다")
            except ib.ImageCallBudgetExceeded:
                got.append("막혔다")

        with canary_image_scope(cap=0):
            with ThreadPoolExecutor(max_workers=4) as pool:
                for _ in range(4):
                    pool.submit(worker)
        assert got == ["막혔다"] * 4

    def test_the_bind_helper_does_not_erase_the_shared_install(self):
        """★`run_with_budget` 은 「이전 것」을 되돌린다 — 같이 보는 저장소에서
        그 이전 것은 **바로 이 예산**이라 되돌려도 그대로여야 한다."""
        with canary_image_scope(cap=2) as budget:
            def inner():
                ib.reserve_current_call(source="test.inner")

            with ThreadPoolExecutor(max_workers=1) as pool:
                pool.submit(ib.bind_current_budget(inner)).result()
            assert ib.get_current_budget() is budget
            ib.reserve_current_call(source="test.after")
            assert budget.snapshot()["used"] == 2

    def test_it_puts_the_thread_local_back(self):
        with canary_image_scope(cap=1):
            pass
        assert isinstance(ib._local, threading.local)
        assert ib.get_current_budget() is None


class TestItRefusesToStealSomeoneElsesCount:
    def test_nesting_stops(self):
        with canary_image_scope(cap=1):
            with pytest.raises(RuntimeError, match="겹쳐"):
                with canary_image_scope(cap=1):
                    pass

    def test_an_existing_install_stops(self):
        ib.install_budget(ib.ImageCallBudget(cap=3))
        try:
            with pytest.raises(RuntimeError, match="겹쳐"):
                with canary_image_scope(cap=1):
                    pass
        finally:
            ib.uninstall_budget()


class TestWhatItReports:
    def test_the_delta_counts_both_bought_and_refused(self):
        assert image_delta({"used": 1, "denied": 0},
                           {"used": 1, "denied": 2}) == {
            "image_counted": 0, "image_denied": 2}

    def test_the_named_door_is_really_in_that_file(self):
        """★글로 적은 자리가 **코드에 있는지** 본다 — 낡으면 거짓말이 된다.

        `uncovered_paths()` 가 문 셋을 「없다」고 적고 있던 것을 오늘 잡았다.
        글과 코드가 갈라지면 **글만 읽고 틀린 판단**을 한다.
        """
        got = image_doors()["in_this_closure"]["floor_plan_render"]
        src = resolve(got["file"])
        assert src.exists(), src
        body = src.read_text(encoding="utf-8")
        for site in got["sites"]:
            assert f'reserve_current_call(source="floor_plan_render.{site}")' \
                in body, f"{site} 자리가 코드에 없다"
        assert body.count("reserve_current_call(source=") == len(got["sites"])
