"""배경 자리에 **셋**(기존 판 + 맥락 + 상세)이 실릴 수 있나. ★유료 0.

## 왜 이 시험이 있나

사용자 확정: 「참조 이미지 여러 개로 해도 돼」 · 「둘을 찾아서 참조 두 개
이상으로」. `location_part` 는 **맥락**(그 장소)과 **상세**(그 부분) 둘을
찾아 같은 `background` 묶음에 붙인다.

★내가 앞서 「개수 제한 없이 이미 된다」고 보고했는데 **틀렸다**
(Codex 정정 2026-08-31). 근거로 든 `image_asset` 의 1~6장은 **모든 kind 합계**
였다. 일반 씬 배경 경로는 chain **이냐** prev **이냐** 한 장이고
`ref_contract_validator:318` 이 「shot 의 background ref slot 은 **1개**」라고
적고 있다.

★★그런데 **선례가 있다** — 야외 직접 경로는 이미 지도+사진 **두 장**을 같은
`background` kind 로 넣는다(`scene_generation_coordinator:495-515`).
그래서 「구조가 못 받는다」가 아니라 「**일반 경로가 한 장만 넣는다**」가
정확한 사실이다.

여기서는 **계약 층이 셋을 받는지**를 끝점(`validate_attached_refs`)에서
확인한다. 활성화 전에 봐야 하는 것의 **첫째**다 (Codex).
"""
from __future__ import annotations

import pytest

from app.core.errors import StaleUpstreamError
from app.core.ref_contract_validator import (RefContractError,
                                             validate_attached_refs)

BG = "L01B01"
#: ★조사에서 온 참조는 **어디서 왔는지**를 값에 남긴다 — 야외 경로가
#:  `outdoor_canon:<place_id>` 를 쓰는 것과 같은 꼴.
CTX = "grounding:LP01:context"
DET = "grounding:LP01:detail"


def _rpc(**over):
    base = {
        "asset_requirements": {
            "required_refs": [
                {"kind": "background", "id": BG, "policy": "required"},
            ],
            "readiness_policy": "na",
        },
    }
    base["asset_requirements"].update(over)
    return base


def _call(meta, *, kinds=("background",), close=False):
    labeled = [(f"Image {i+1}", b"x") for i in range(len(meta))]
    validate_attached_refs(_rpc(), labeled, list(meta), "프롬프트",
                           close, chain_bg_lookup=lambda _b: None,
                           reference_phrase_kinds=list(kinds))


class TestTheContractLayerTakesMoreThanOne:
    def test_one_background_passes(self):
        _call([("background", BG)])

    def test_the_outdoor_precedent_two_passes(self):
        """★이미 production 에서 도는 모양 — 지도+사진."""
        _call([("background", "outdoor_canon:P1"),
               ("background", "outdoor_canon:P1")])

    def test_three_backgrounds_pass(self):
        """★★이 판에서 필요한 모양 — 기존 판 + 맥락 + 상세."""
        _call([("background", BG), ("background", CTX), ("background", DET)])

    def test_the_parallel_lists_must_stay_the_same_length(self):
        """★positive control — 짝이 어긋나면 **선다**."""
        with pytest.raises(RefContractError) as e:
            validate_attached_refs(
                _rpc(), [("Image 1", b"x")],
                [("background", BG), ("background", CTX)],
                "프롬프트", False, chain_bg_lookup=lambda _b: None,
                reference_phrase_kinds=["background"])
        assert "length mismatch" in str(e.value)

    def test_a_declared_kind_with_nothing_attached_stops(self):
        """★positive control — 선언만 하고 안 붙이면 선다.

        ★`RefContractError` 가 아니라 `StaleUpstreamError` 로 선다 — 이쪽이
        더 정확한 진단(위 스텝이 낡았다)이다. 둘 다 **선다**는 것이 요점.
        """
        with pytest.raises((RefContractError, StaleUpstreamError)):
            validate_attached_refs(
                _rpc(), [("Image 1", b"x")], [("character", "C01")],
                "프롬프트", False, chain_bg_lookup=lambda _b: None,
                reference_phrase_kinds=["background"])


class TestTheBundleKeepsItsOwnProvenance:
    """★참조마다 **어디서 왔는지**가 남아야 감사가 된다 (Codex).

    `ref_role_metadata` 에 `grounding_subject_id` · `lp_final_id` ·
    `purpose(context|detail)` · `parent_relation` · `asset_lineage`.
    여기서는 **값이 서로 구별되는지**만 본다 — 붙이는 코드는 D 에서 짓는다.
    """

    def test_context_and_detail_are_distinguishable(self):
        assert CTX != DET
        assert CTX.rsplit(":", 1)[-1] == "context"
        assert DET.rsplit(":", 1)[-1] == "detail"

    def test_they_share_the_same_kind(self):
        """★새 **슬롯**을 만들지 않는다 — 같은 `background` kind 를 쓴다.

        ★★★**맥락·상세는 덧붙는 것이지 기존 배경판을 대신하지 못한다**
        (실측 2026-08-31). 요구된 `background` id 를 안 붙이고 조사 참조만
        넣으면 `StaleUpstreamError` 로 **선다** — 「위 스텝이 낡았다」로 읽는다.
        D 는 이것을 **더하는** 설계여야 한다.
        """
        _call([("background", BG), ("background", CTX), ("background", DET)])

    def test_the_bundle_cannot_replace_the_required_plate(self):
        """★positive control — 위 사실을 **실제로** 확인한다."""
        with pytest.raises((RefContractError, StaleUpstreamError)):
            _call([("background", CTX), ("background", DET)])


class TestThroughTheProductionHelper:
    """★★★**production 조립 helper 를 태운다** (Codex BLOCK 3 2026-08-31).

    앞 시험들은 목록을 **내 손으로 지어** validator 만 불렀다 — 그것은
    「그릇이 셋을 받는다」까지만 잰다. 실제로 붙이는 코드가 네 평행 목록을
    어떻게 다루는지는 못 본다.

    `_insert_outdoor_canon_refs` 는 **이미 production 에서 도는** 덧붙임
    helper 다(야외 직행 — 사진+지도 두 장을 같은 `background` kind 로).
    D 의 `location_part` 묶음도 **같은 모양**으로 짓는다. 그래서 여기서
    그 helper 의 계약을 잠그고, 그 위에 조사 묶음을 쌓아 끝점까지 본다.
    """

    @staticmethod
    def _base():
        """기존 배경판 하나가 이미 붙어 있는 상태 — chain_bg 가 넣는 모양."""
        return ([("배경판", b"plate")], [("background", BG)],
                ["background_chain_ref"], [{"pipeline_role": "background_render"}])

    @staticmethod
    def _od():
        return {"map_bytes": b"map", "master_bytes": b"photo",
                "place_id": "P1", "map_asset_id": "a-map",
                "master_asset_id": "a-photo", "prompt_version": "1"}

    def test_the_production_helper_is_additive_and_parallel(self):
        """★기존 것을 **안 지우고** 네 목록이 같이 늘어난다."""
        from app.services.scene_generation_coordinator import (
            _insert_outdoor_canon_refs)

        lr, am, rr, rm = self._base()
        _insert_outdoor_canon_refs(lr, am, rr, rm, self._od(),
                                   scene_index=1, shot_index=1, where="t")
        assert len(lr) == len(am) == len(rr) == len(rm) == 3, \
            f"★네 목록이 안 맞는다: {len(lr)} {len(am)} {len(rr)} {len(rm)}"
        assert ("background", BG) in am, "★기존 배경판이 사라졌다"
        assert sum(1 for k, _ in am if k == "background") == 3, \
            "★셋이 같은 background kind 로 안 붙었다"
        assert rr == ["outdoor_canon_photo_ref", "outdoor_canon_map_ref",
                      "background_chain_ref"], f"★역할 차례가 다르다: {rr}"

    def test_each_ref_keeps_its_own_metadata(self):
        """★참조마다 **어디서 왔는지**가 남는다 — D 가 따를 모양."""
        from app.services.scene_generation_coordinator import (
            _insert_outdoor_canon_refs)

        lr, am, rr, rm = self._base()
        _insert_outdoor_canon_refs(lr, am, rr, rm, self._od(),
                                   scene_index=1, shot_index=1, where="t")
        roles = {m.get("pipeline_role") for m in rm}
        assert roles == {"background_render", "outdoor_canon_photo",
                         "outdoor_canon_map"}, f"★출처가 뭉쳤다: {roles}"
        assert all(m.get("asset_id") for m in rm if "outdoor" in
                   str(m.get("pipeline_role"))), "★자산 계보가 빠졌다"

    def test_three_from_the_helper_pass_the_validator(self):
        """★★끝점 — production helper 산출이 **그대로** validator 를 지난다."""
        from app.services.scene_generation_coordinator import (
            _insert_outdoor_canon_refs)

        lr, am, rr, rm = self._base()
        _insert_outdoor_canon_refs(lr, am, rr, rm, self._od(),
                                   scene_index=1, shot_index=1, where="t")
        validate_attached_refs(_rpc(), lr, am, "프롬프트", False,
                               chain_bg_lookup=lambda _b: None,
                               reference_phrase_kinds=["background"])

    def test_a_grounding_pair_stacks_on_top(self):
        """★D 가 할 일 — 그 위에 **맥락+상세**를 같은 모양으로 더 얹는다."""
        from app.services.scene_generation_coordinator import (
            _insert_outdoor_canon_refs)

        lr, am, rr, rm = self._base()
        _insert_outdoor_canon_refs(lr, am, rr, rm, self._od(),
                                   scene_index=1, shot_index=1, where="t")
        for label, val, role, purpose in (
                ("맥락", CTX, "grounding_context_ref", "context"),
                ("상세", DET, "grounding_detail_ref", "detail")):
            lr.insert(0, (label, b"x"))
            am.insert(0, ("background", val))
            rr.insert(0, role)
            rm.insert(0, {"pipeline_role": role, "purpose": purpose,
                          "lp_final_id": "LP01",
                          "grounding_subject_id": "LP01"})
        assert len(lr) == len(am) == len(rr) == len(rm) == 5
        validate_attached_refs(_rpc(), lr, am, "프롬프트", False,
                               chain_bg_lookup=lambda _b: None,
                               reference_phrase_kinds=["background"])
        # ★purpose 로 갈라 볼 수 있다 — 감사에 필요하다
        assert {m.get("purpose") for m in rm if m.get("purpose")} == \
            {"context", "detail"}


class TestTheWaiverThatWouldHideAMissingGroundingRef:
    """★★★현재 validator 는 **조사 배경이 없어도 지난다** (Codex BLOCK 3).

    실측 둘 —

        :300  `if not is_close_framing:` — 클로즈업이면 배경 검사가 **통째로**
              사라진다. 부분 클로즈업에서 LP 상세가 안 붙어도 안 걸린다
        :318  「slot 은 1개이므로 required 복수여도 **전체 충족 처리**」 —
              합성/야외 **한 장**이 요구된 배경 **전부**를 면제한다

    ★여기서는 그 두 구멍이 **실제로 있다**는 것만 잠근다. 막는 것은 D 다 —
     조사에서 온 `selected` 는 exact attachment 를 요구하게 하고, LP 상세는
     **클로즈업에서도** 검증한다.
    """

    def test_close_framing_skips_the_background_check(self):
        """★구멍 ① — 클로즈업이면 요구된 배경이 없어도 지난다."""
        validate_attached_refs(_rpc(), [("Image 1", b"x")],
                               [("character", "C01")], "프롬프트",
                               True,          # ★close framing
                               chain_bg_lookup=lambda _b: None,
                               reference_phrase_kinds=[])

    def test_one_synthetic_background_waives_all_required(self):
        """★구멍 ② — 합성 배경 한 장이 요구된 배경 **둘**을 다 면제한다."""
        rpc = {"asset_requirements": {
            "required_refs": [
                {"kind": "background", "id": BG, "policy": "required"},
                {"kind": "background", "id": "L02B02", "policy": "required"},
            ], "readiness_policy": "na"}}
        validate_attached_refs(
            rpc, [("Image 1", b"x")],
            [("background", "space_set_bg:g1:plate1")], "프롬프트", False,
            chain_bg_lookup=lambda _b: None,
            reference_phrase_kinds=["background"])
