"""★통합 canary 범위(배경 on + outdoor on → scene_detail)의 이미지 계약 (Codex 2026-09-03 06:10).

이미지를 사는 스텝은 floor_plan_render 하나 · 정상 6 · run-wide 18. 다른 이미지 source 가 계획에 보이면 provider 전에 선다.
재산정한 중앙 step cap 이 run 전체의 남은 ceiling 을 넘으면 그때만 선다."""
from __future__ import annotations

import pytest

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

SC = {"mode": "v2_chunk", "fixture": "period_episode", "target": "scene_detail", "background": "on", "still_recipe": "off", "outdoor": "on"}


class TestTheIntegrationScope:
    def test_the_image_approval_is_eighteen_and_only_the_floor_plan_render_may_buy(self, monkeypatch):
        assert cr.approved_images_for(SC) == 18
        monkeypatch.setattr(cr, "axis_actual", lambda axis: {"background": "on", "still_recipe": "off", "outdoor": "on"}[axis])
        sc = cr.scenario(**SC)
        built = cr.build_plan(sc, run_id=None)
        assert cr.image_steps_in_plan(built) == ["floor_plan_render"]
        got = cr.assert_image_sources(built, sc)
        assert got["image_steps"] == ["floor_plan_render"]
        rows = {r["step"]: r for r in built["images"]["rows"]}
        assert rows["floor_plan_render"]["normal"] == 6 and rows["floor_plan_render"]["hard"] == 18

    def test_another_image_source_in_the_plan_stops(self):
        built = {"images": {"rows": [{"step": "floor_plan_render", "units_cap": 6, "normal_per_unit": 1},
                                     {"step": "scene_image_pipeline", "units_cap": 10, "normal_per_unit": 1}]}}
        with pytest.raises(cr.ScopeMismatch):
            cr.assert_image_sources(built, SC)

    def test_a_scope_without_an_entry_only_records(self):
        """허용표에 없는 범위는 제 문(이미지 승인 0)에서 제 문구로 선다 — 여기서 먼저 서면 그 문구를 가린다."""
        built = {"images": {"rows": [{"step": "floor_plan_render", "units_cap": 6, "normal_per_unit": 1}]}}
        got = cr.assert_image_sources(built, {**SC, "fixture": "canary_one_scene"})
        assert got["outside"] == ["floor_plan_render"]


class TestTheRecomputedCapRespectsTheRemainingCeiling:
    def test_the_hook_source_stops_only_when_the_new_cap_exceeds_what_is_left(self):
        import inspect
        src = inspect.getsource(cp.run_pipeline)
        assert "if _new > _left:" in src and "raise CanaryStopped" in src.split("if _new > _left:")[1][:600]
