"""아웃룩 고증 사진이 **인물 아웃룩 자리**에 붙나. ★유료 0.

Codex 2026-09-01 —
> outlook=none 은 최종 상태로 승인하지 않습니다. 새 슬롯·새 구매자는 만들지
> 말고, 기존 `character_outlook` 슬롯에 중앙 조사 이미지를 additive
> exact-attachment 하는 문까지 닫으십시오.

여섯 축 —
  ①`C##O##` 는 **검증된 결속**에서만 나온다 (이름·부분문자열 0)
  ②`outlook_pairs` 가 정본 — 조사가 **새 짝을 만들지 않는다**
  ③기존 `character_outlook` 요구를 그대로 쓴다 (새 kind 0)
  ④기존 합성본이 고증 부착을 **대신하지 못한다**
  ⑤못 구했으면 부착 0 · 막힘 0
  ⑥합성본과 고증 사진이 **같이** 들어가도 차례·역할·값이 보존된다
"""
from __future__ import annotations

import hashlib
from pathlib import Path

import pytest

from app.modules.pipeline import grounding_bundle_projection as bp
from app.modules.pipeline import grounding_reference_bundle as gb
from app.modules.pipeline import grounding_sidecar_writer as sw
from app.services.prompt_service import (REF_ROLE_VALUES,
                                         make_labeled_ref_payload,
                                         resolve_ref_roles)
from app.services.scene_generation_coordinator import build_scene_attached_refs

from tests.grounding.test_production_assembly_endpoint import (_member, _run,
                                                               _sha, _still,
                                                               _svc)

COAT = b"1960s-overcoat-photo"


def _row(*, oid="O09", cid="C08", outcome=gb.OUTCOME_SELECTED, ident="acq-1",
         fidelity="verified"):
    """중앙 조사 한 줄 — ★`ledger_row` 는 아웃룩 결속이 낸 모양.

    ★★★2026-09-02: `selected` 만으로는 **안 붙는다**. coarse 심판은
    「무엇인가·보이는가」만 봤고, 그 사진이 그 시대·그 지역 것인지는 아무도
    안 봤다. 소비자 문이 **두 축을 다** 본다 (Codex BLOCK). 그래서 기본을
    `verified` 로 두되, **미확인이 안 붙는 것**을 따로 잠근다 — 안 그러면
    이 fixture 가 결함을 숨긴다.
    """
    return {"research_subject_id": f"rs-{oid}", "identity": ident,
            "outcome": outcome, "why": "", "why_unbought": None,
            "grounding_fidelity": {"state": fidelity},
            "ledger_row": {"owner_type": "outlook", "final_id": oid,
                           "parent_final_id": cid}}


def _cp(rows):
    return {"data": {"rows": rows}}


def _card(*outlook_ids):
    return {"asset_requirements": {"required_refs": [
        {"kind": "character_outlook", "id": x, "policy": "required"}
        for x in outlook_ids]}}


def _payload_of(card, *, base=None):
    """★★**production 5-tuple** 의 payload 를 그대로 꺼낸다.

    손으로 `ref_role_metadata` 를 만들지 않는다 — 조립이 실은 것만 본다
    (Codex BLOCK 2026-09-02: 앞 판 시험은 `[{}]` 를 넣어 일반 문구만 봤다).

    Args:
        base: 이 샷에 **이미 붙어 있는** 참조들(기존 합성본 등). 실제
            `reference_svc.resolve_refs_for_prompt_set` 이 내는 자리다.
    """
    svc = _svc()
    if base is not None:
        svc.resolve_refs_for_prompt_set.return_value = base
    got = build_scene_attached_refs(
        still=_still(), episode_id="ep",
        still_data={"scene_index": 8, "shot_index": 4,
                    "still_frame_prompt": "a shot", "beat_title": "",
                    "render_prompt_card": card},
        visible_entities=[], ref_image_map={},
        cached_style_context="S", cached_entity_text_map={},
        scene_paths_by_index_by_id={}, location_scene_history={},
        background_chain_bg_map={}, dep_detail_map={},
        staging={"camera_direction": "medium shot", "framing_scale": "medium"},
        entity_lookup={}, project_id="p", project_config={},
        reference_svc=svc, return_payload=True)
    return got[4]


def _write(card, rows, root):
    p = Path(root) / "coat.png"
    p.write_bytes(COAT)
    return sw.write_for_shot(
        card, _cp(rows), [],
        content_sha_of=lambda r: _sha(COAT),
        coordinate_of=lambda r: {"source": "file", "path": str(p)})


class TestTheIdComesFromTheVerifiedBinding:

    def test_it_joins_the_parent_and_the_facet(self):
        assert bp.outlook_ref_id(_row()) == "C08O09"

    def test_a_row_without_a_parent_makes_nothing(self):
        r = _row()
        r["ledger_row"].pop("parent_final_id")
        assert bp.outlook_ref_id(r) is None

    def test_a_wrong_parent_kind_makes_nothing(self):
        """★부모가 인물이 아니면 **안 지어낸다**."""
        r = _row(cid="L01")
        assert bp.outlook_ref_id(r) is None

    def test_another_lane_is_not_an_outlook(self):
        assert bp.outlook_ref_id({"ledger_row": {
            "owner_type": "location_part", "final_id": "LP01",
            "parent_final_id": "L01"}}) is None


class TestOnlyPairsTheShotActuallyUses:
    """★★조사가 **새 짝을 만들지 않는다** — 카드가 정본이다."""

    def test_a_pair_the_card_does_not_ask_for_is_skipped(self, tmp_path):
        card = _card("C01O02")               # ★다른 짝만 쓴다
        assert _write(card, [_row()], tmp_path) == 0
        assert gb.RPC_MEMBERS_KEY not in card, "★카드를 건드렸다"

    def test_a_card_with_no_outlook_is_untouched(self, tmp_path):
        card = _card()
        assert _write(card, [_row()], tmp_path) == 0
        assert card == _card(), "★아웃룩을 안 쓰는 컷인데 카드가 달라졌다"

    def test_the_used_pair_is_written(self, tmp_path):
        card = _card("C08O09")
        assert _write(card, [_row()], tmp_path) == 1
        req = card[gb.RPC_REQUIRED_KEY]
        assert [r["kind"] for r in req] == ["character_outlook"], (
            "★새 kind 를 만들었다")


class TestItLandsThroughTheRealAssembly:

    def test_the_photo_reaches_the_outlook_slot(self, tmp_path):
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        _p, refs, meta, _k = _run(card)
        assert len(refs) == 1, f"★{len(refs)}장"
        assert meta[0][0] == "character_outlook"
        got = gb.parse_meta_value(meta[0][1])
        assert got["subject_final_ids"] == ["C08O09"]

    def test_it_uses_an_already_declared_role(self, tmp_path):
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        assert sw.roles_in(card) == [gb.OUTLOOK_ROLE]
        assert gb.OUTLOOK_ROLE in REF_ROLE_VALUES, "★선언 안 된 역할이다"
        assert gb.undeclared_roles(REF_ROLE_VALUES) == []

    def test_the_prompt_tells_the_model_whose_outfit_it_is(self, tmp_path):
        """★★★끝점 — **조립이 만든 metadata** 로 실제 `Image N` 지시까지.

        ★앞 판 시험은 `ref_role_metadata=[{}]` 와 손으로 만든 label 을 넣어
        일반 문구만 봤다 — 「실제 prompt 끝점」이 아니었다 (Codex BLOCK).
        이제 production 5-tuple 의 payload 를 그대로 태운다.
        """
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        payload = _payload_of(card)
        got = resolve_ref_roles(payload)
        text = got.roles_text + "\n" + "\n".join(got.ref_instructions)
        assert "Reference image 1: standalone outfit/costume reference for " \
               "C08O09" in text, text
        assert "dress C08O09 in the outfit shown in image 1" in text, text
        assert "do not take face, body, or identity from it" in text, text

    def test_nothing_lands_when_it_was_not_found(self, tmp_path):
        card = _card("C08O09")
        n = _write(card, [_row(outcome=gb.OUTCOME_UNAVAILABLE)], tmp_path)
        _p, refs, meta, _k = _run(card)
        assert refs == [] and meta == [], "★못 구했는데 붙었다"
        assert n == 0, "★못 구한 것을 요구로 적었다"


class TestTwoCharactersNeverGetSwapped:
    """★★★인물 둘·복장 둘 — `Image N` 이 **뒤바뀌지 않는다**."""

    def _two(self, tmp_path):
        card = _card("C01O01", "C02O02")
        a, b = Path(tmp_path) / "a.png", Path(tmp_path) / "b.png"
        a.write_bytes(b"coat-A")
        b.write_bytes(b"coat-B")
        photo = {"C01O01": (a, b"coat-A"), "C02O02": (b, b"coat-B")}

        def _sha_of(r):
            return _sha(photo[bp.outlook_ref_id(r)][1])

        def _coord(r):
            return {"source": "file",
                    "path": str(photo[bp.outlook_ref_id(r)][0])}

        rows = [_row(oid="O01", cid="C01", ident="acq-1"),
                _row(oid="O02", cid="C02", ident="acq-2")]
        sw.write_for_shot(card, _cp(rows), [], content_sha_of=_sha_of,
                          coordinate_of=_coord)
        return card, photo

    def test_each_image_names_its_own_pair(self, tmp_path):
        card, photo = self._two(tmp_path)
        payload = _payload_of(card)
        got = resolve_ref_roles(payload)
        assert len(payload.labeled_refs) == 2, "★두 장이 아니다"
        # ★그림 bytes 로 짝을 되짚는다 — 문구 차례를 믿지 않는다
        for i, (_label, raw) in enumerate(payload.labeled_refs, 1):
            mine = [sid for sid, (_p, b) in photo.items() if b == raw]
            assert len(mine) == 1, mine
            assert f"dress {mine[0]} in the outfit shown in image {i}" in \
                "\n".join(got.ref_instructions), (mine, i, got.ref_instructions)

    def test_one_photo_covering_two_outfits_stays_two_attachments(
            self, tmp_path):
        """★같은 사진이라도 **주인마다** 붙는다 — 그래야 지시가 성립한다."""
        card = _card("C01O01", "C02O02")
        p = Path(tmp_path) / "same.png"
        p.write_bytes(COAT)
        sw.write_for_shot(
            card, _cp([_row(oid="O01", cid="C01", ident="acq-1"),
                       _row(oid="O02", cid="C02", ident="acq-2")]), [],
            content_sha_of=lambda r: _sha(COAT),
            coordinate_of=lambda r: {"source": "file", "path": str(p)})
        payload = _payload_of(card)
        assert len(payload.labeled_refs) == 2, "★합쳐져 주인이 둘이 됐다"
        got = resolve_ref_roles(payload)
        joined = "\n".join(got.ref_instructions)
        assert "dress C01O01" in joined and "dress C02O02" in joined


class TestTheRealCompositeAndTheGroundingPhotoCoexist:
    """★★진짜 기존 합성본(`outfit_ref_inline`) + 고증 사진이 한 payload 에."""

    def _both(self, tmp_path):
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        composite = make_labeled_ref_payload(
            labeled_refs=[("Image (outfit inline): C08O09", b"composite")],
            ref_roles=["outfit_ref_inline"],
            ref_role_metadata=[{"subject_final_ids": ["C08O09"]}],
            attached_meta=[("character_outlook", "C08O09")])
        return _payload_of(card, base=composite)

    def test_both_survive_with_their_own_roles(self, tmp_path):
        payload = self._both(tmp_path)
        assert payload.ref_roles == [gb.OUTLOOK_ROLE, "outfit_ref_inline"], (
            f"★차례·역할이 어긋났다: {payload.ref_roles}")
        kinds = [k for k, _v in payload.attached_meta]
        assert kinds == ["character_outlook", "character_outlook"]

    def test_the_exact_grounding_assertion_still_holds(self, tmp_path):
        payload = self._both(tmp_path)
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        gb.assert_from_rpc(card, payload.attached_meta)

    def test_the_composite_alone_does_not_satisfy_it(self, tmp_path):
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        with pytest.raises(gb.BundleContractError):
            gb.assert_from_rpc(card, [("character_outlook", "C08O09")])

    def test_the_two_instructions_say_different_things(self, tmp_path):
        payload = self._both(tmp_path)
        got = resolve_ref_roles(payload)
        joined = "\n".join(got.ref_instructions)
        assert "dress C08O09 in the outfit shown in image 1" in joined
        assert "match the person's identity and outfit" in joined, (
            "★합성본 지시가 사라졌다")


class TestAnExistingCompositeDoesNotSatisfyIt:
    """★★★합성본이 같은 kind·id 로 붙었다고 고증 부착이 채워지지 않는다."""

    def test_the_same_kind_and_id_is_not_enough(self, tmp_path):
        card = _card("C08O09")
        _write(card, [_row()], tmp_path)
        required = [(r["kind"], r["value"]) for r in card[gb.RPC_REQUIRED_KEY]]
        # ★기존 합성본이 붙은 판 — 같은 자리, 같은 신원, **다른 값**
        composite = [("character_outlook", "C08O09")]
        with pytest.raises(gb.BundleContractError):
            gb.assert_grounding_attached(required, composite)

    def test_the_grounding_value_carries_the_acquisition_identity(
            self, tmp_path):
        """★획득 신원이 값에 실린다 — 옛 자산이 새 의무를 못 만족시킨다."""
        card = _card("C08O09")
        _write(card, [_row(ident="acq-A")], tmp_path)
        a = card[gb.RPC_REQUIRED_KEY][0]["value"]
        card_b = _card("C08O09")
        _write(card_b, [_row(ident="acq-B")], tmp_path)
        assert a != card_b[gb.RPC_REQUIRED_KEY][0]["value"]


class TestBothLanesInOneShot:
    """★★배경 묶음과 아웃룩 사진이 **같이** 들어가도 안 섞인다."""

    def test_kinds_roles_and_meta_are_all_preserved(self, tmp_path):
        card = _card("C08O09")
        p = Path(tmp_path) / "coat.png"
        p.write_bytes(COAT)
        members = [_member("detail", b"barbershop-sign", root=tmp_path)]
        members += bp.outlook_members_from_rows(
            [_row()], required_ref_ids=["C08O09"],
            content_sha_of=lambda r: _sha(COAT),
            coordinate_of=lambda r: {"source": "file", "path": str(p)})
        gb.write_sidecar(card, members)
        _pr, refs, meta, _k = _run(card)
        assert len(refs) == 2, f"★{len(refs)}장"
        assert {k for k, _v in meta} == {"background", "character_outlook"}
        plan, _req = gb.plan_bundle(gb.members_from_rpc(card))
        assert {p_["role"] for p_ in plan} == {
            "grounding_part_detail_ref", gb.OUTLOOK_ROLE}

    def test_the_same_bytes_in_two_slots_stay_two(self, tmp_path):
        """★같은 사진이라도 **자리가 다르면** 합치지 않는다."""
        card = _card("C08O09")
        p = Path(tmp_path) / "same.png"
        p.write_bytes(COAT)
        members = [_member("detail", COAT, root=tmp_path)]
        members += bp.outlook_members_from_rows(
            [_row()], required_ref_ids=["C08O09"],
            content_sha_of=lambda r: _sha(COAT),
            coordinate_of=lambda r: {"source": "file", "path": str(p)})
        gb.write_sidecar(card, members)
        _pr, refs, meta, _k = _run(card)
        assert len(refs) == 2, f"★자리가 다른데 합쳐졌다: {len(refs)}"
