"""★야외 그룹 상한은 **producer 산출**을 provider 앞에서 세는 문이다 (Codex BLOCK 2026-09-03).

앞 판의 `OUTDOOR_GROUP_CAP` 은 dims→logical cap 에만 들어가 「셋째 그룹이면 앞 둘을 산 뒤 provider 경계에서
서는」 모양이었다. 이제 `background_classify` CP 의 실외 그룹 수를 production 술어(`outdoor_groups_of`) 로
세어 넘으면 **사지 않고** `ScopeMismatch` 로 선다. 그룹 수(계측 분모)와 unique location 수(보충 행 분모)를
같이 낸다."""
from __future__ import annotations

import json

import pytest

from tools.grounding_audit import canary_run as cr


def _cp(root, groups):
    d = root / "projects" / "p" / "checkpoints" / "episodes" / "e" / "background_classify"
    d.mkdir(parents=True)
    (d / "manifest.json").write_text(json.dumps({"data": {"building_groups": groups}}), encoding="utf-8")


def _group(gid, members):
    return {"group_id": gid, "members": [{"loc_id": lid, "is_indoor": indoor} for lid, indoor in members]}


@pytest.fixture
def run_root(tmp_path, monkeypatch):
    monkeypatch.setattr(cr.ci, "root_dir", lambda rid: tmp_path)
    return tmp_path


STEPS = [s for s, u in cr.METERING_UNITS.items() if u == cr.UNIT_OUTDOOR_GROUP]


class TestTheGateReadsTheProducerOutput:
    def test_two_outdoor_groups_under_a_cap_of_two_pass_with_both_denominators(self, run_root):
        _cp(run_root, [_group("G1", [("L01", False), ("L02", False)]),
                       _group("G2", [("L02", False)]),
                       _group("G3", [("L03", True)])])          # ★실내만 → 실외 그룹이 아니다
        got = cr.assert_outdoor_group_cap_covers("r", {"outdoor_group_cap": 2}, set(), STEPS)
        assert got["checked"] is True
        assert got["outdoor_groups"] == 2                       # 계측·상한 분모
        assert got["unique_outdoor_locations"] == 2             # 보충 행 분모 (L02 는 두 그룹이 가리켜도 하나)
        assert got["outdoor_loc_ids"] == ["L01", "L02"]

    def test_a_third_outdoor_group_stops_before_buying(self, run_root):
        _cp(run_root, [_group("G1", [("L01", False)]), _group("G2", [("L02", False)]),
                       _group("G3", [("L03", False)])])
        with pytest.raises(cr.ScopeMismatch) as e:
            cr.assert_outdoor_group_cap_covers("r", {"outdoor_group_cap": 2}, set(), STEPS)
        assert "실외 그룹 3" in str(e.value) and "OUTDOOR_GROUP_CAP 2" in str(e.value)

    def test_without_the_cp_the_declared_value_stands(self, run_root):
        got = cr.assert_outdoor_group_cap_covers("r", {"outdoor_group_cap": 2}, set(), STEPS)
        assert got["checked"] is False and got["outdoor_group_cap"] == 2

    def test_the_predicate_is_productions_own(self):
        """술어를 두 곳에 적지 않는다 — 스텝과 문이 같은 함수를 부른다."""
        import inspect
        from app.core.steps import outdoor_place_spec_step as st
        assert "outdoor_groups_of(classify_cp)" in inspect.getsource(st)
        assert "outdoor_groups_of" in inspect.getsource(cr.assert_outdoor_group_cap_covers)


class TestTheGateIsCalledRightBeforeTheStep:
    def test_the_outdoor_unit_step_calls_the_gate(self, run_root, monkeypatch):
        seen = []
        monkeypatch.setattr(cr, "assert_outdoor_group_cap_covers",
                            lambda *a: seen.append(a) or {"checked": True, "outdoor_groups": 1})
        got = cr.producer_cap_gate_before(STEPS[0], run_id="r", dims={"outdoor_group_cap": 2},
                                          done=set(), metered=STEPS)
        assert seen and got["gate"] == "assert_outdoor_group_cap_covers" and got["unit"] == cr.UNIT_OUTDOOR_GROUP

    def test_a_forced_finished_step_is_still_gated_and_a_single_unit_step_is_not(self, run_root, monkeypatch):
        """★뒤집음: 끝난 스텝도 지문 어긋남으로 force 되면 산다 — 부르는 쪽이 살 스텝에만 부르므로 문은 `done` 을 안 믿는다."""
        seen = []
        monkeypatch.setattr(cr, "assert_outdoor_group_cap_covers",
                            lambda rid, dims, done, metered: seen.append(set(done)) or {"checked": True})
        got = cr.producer_cap_gate_before(STEPS[0], run_id="r", dims={}, done={STEPS[0]}, metered=STEPS)
        assert got is not None and seen and STEPS[0] not in seen[0]
        single = next(s for s, u in cr.METERING_UNITS.items() if u == cr.UNIT_SINGLE)
        assert cr.producer_cap_gate_before(single, run_id="r", dims={}, done=set(), metered=[single]) is None

    def test_every_producer_gate_names_a_real_function(self):
        for unit, name in cr.PRODUCER_CAP_GATES.items():
            assert callable(getattr(cr, name)), (unit, name)
