"""GROUNDING-V2 §2-4b — **공개 카드 조립**을 실제로 태운다.

★★★조립 함수를 직접 부르는 시험은 「조립 자리 단절」을 **다시 못 잡는다**
(Codex). 실제 `build_render_prompt_card` 를 태워, 조사가 올린 C/P 가 최종
`asset_requirements.required_refs` 에 나타나는지 본다.
"""
import json

import pytest

from app.core.steps.render_prompt_card import build_render_prompt_card


def _policy(forced):
    """스텝이 만드는 모양 그대로 — `grounding` 칸이 정본이다."""
    return {
        "policy": {sid: {"mode": "reference_required",
                         "reason": "조사", "entity_type": "prop",
                         "visible_shot_count": 1, "provisional": False}
                   for sid in forced},
        "grounding": {"research_required_short_ids": sorted(forced),
                      "override_used": False},
    }


def _card(*, forced=None, visible=("C01",), outlooks=None):
    return build_render_prompt_card(
        scene_index=3, shot_index=1,
        seg={}, shot_info={"camera_direction": "medium shot"},
        visible_entities=list(visible),
        outlook_pairs=(outlooks if outlooks is not None
                       else [{"character_id": "C01", "outlook_id": "O02"}]),
        perception_mode=None,
        staging={"camera_direction": "medium shot",
                 "framing_scale": "medium", "lighting_mood": "warm dim",
                 "key_bg_elements": [], "subject_reference_policy": []},
        bg_id="cb_room", bg_owned=[], bg_camera_meta={"camera_position": "wide"},
        bg_guide="guide", is_close_framing=False, background_mode_on=True,
        fixed_elements=[], previous_shot_refs=[], forward_zoom_targets=[],
        # ★★`if forced` 로 가르면 **빈 집합이 `None` 으로 가서** 두 호출이
        #  완전히 같아진다 — 그러면 byte-identical 시험이 아무것도 안 잰다.
        episode_reference_policy=(_policy(forced) if forced is not None
                                  else None),
    )


def _refs(card):
    return [(r.get("kind"), r.get("id"))
            for r in card["asset_requirements"]["required_refs"]]


class TestTheForcedRefsReachTheRealCard:
    def test_a_forced_prop_appears(self):
        """★★prop 은 원래 `visual_identity` 에서만 나온다 — 조사가 올린 것이
        여기 안 나타나면 **아무 일도 안 일어난 것**이다."""
        card = _card(forced={"P07"}, visible=("C01", "P07"))
        assert ("prop", "P07") in _refs(card)

    def test_a_forced_character_appears(self):
        card = _card(forced={"C01"})
        assert ("character", "C01") in _refs(card)

    def test_a_location_does_not_appear(self):
        """★§4b 밖이다 — prop 으로 새어 나오면 없는 producer 를 요구한다."""
        card = _card(forced={"L01"}, visible=("C01", "L01"))
        kinds = _refs(card)
        assert ("prop", "L01") not in kinds and ("character", "L01") not in kinds

    def test_an_invisible_entity_is_not_forced(self):
        card = _card(forced={"P09"}, visible=("C01",))
        assert ("prop", "P09") not in _refs(card)

    def test_it_does_not_remove_or_double_existing_refs(self):
        """★올리기만 한다 — 기존 것을 내리거나 겹치지 않는다."""
        plain = _card()
        forced = _card(forced={"C01"})
        before, after = _refs(plain), _refs(forced)
        assert set(before) <= set(after), "기존 참조가 사라졌다"
        assert len(after) == len(set(after)), f"중복이 생겼다: {after}"


class TestAnEmptyGroundingLeavesTheCardIdentical:
    """★★legacy 비회귀 — 조사가 아무것도 안 올렸으면 카드가 **바이트 단위로**
    같아야 한다. 카드 hash 가 움직이면 하류가 통째로 다시 돈다."""

    def test_an_empty_grounding_manifest_changes_nothing(self):
        """★★manifest 를 **실제로 넘기고도** 카드가 같아야 한다."""
        a = _card()                    # manifest 없음
        b = _card(forced=set())        # 빈 grounding manifest 를 넘김
        assert json.dumps(a, sort_keys=True, ensure_ascii=False) == \
            json.dumps(b, sort_keys=True, ensure_ascii=False)

    def test_a_policy_with_no_grounding_field_changes_nothing(self):
        a = _card()
        b = build_render_prompt_card(
            scene_index=3, shot_index=1,
            seg={}, shot_info={"camera_direction": "medium shot"},
            visible_entities=["C01"],
            outlook_pairs=[{"character_id": "C01", "outlook_id": "O02"}],
            perception_mode=None,
            staging={"camera_direction": "medium shot",
                     "framing_scale": "medium", "lighting_mood": "warm dim",
                     "key_bg_elements": [], "subject_reference_policy": []},
            bg_id="cb_room", bg_owned=[],
            bg_camera_meta={"camera_position": "wide"},
            bg_guide="guide", is_close_framing=False, background_mode_on=True,
            fixed_elements=[], previous_shot_refs=[], forward_zoom_targets=[],
            episode_reference_policy={"policy": {}},
        )
        assert json.dumps(a, sort_keys=True, ensure_ascii=False) == \
            json.dumps(b, sort_keys=True, ensure_ascii=False)
