"""수동 합성이 **배치와 같은 계약**을 쓴다 (2026-09-20 Codex 감사 BLOCK).

## 무엇이 났나

「몸이 곧 신원인 인물에게는 **옷 사진을 안 준다**」는 수리(3c01121f)가
**배치에만** 들어갔다. 수동 합성(`generate_composite_image`)과 그것을
부르는 엔티티별 재생성(`regenerate_composites_for_entity`)은 그대로였다:

  · 옷 사진을 **derive 여부를 알기 전에 필수로** 읽어 없으면 400
  · 참조에 **기본 몸 + 마네킹 옷 사진 두 장을 무조건** 붙임
  · `outlook_description` 을 **안 넘김** → 파생 문안의 슬롯이 비어
    `<인물> wearing <옷 이름>` 한 줄로 떨어짐

그래서 「커다란 밀짚모자와 거대한 장화」 같은 상세가 최종 생성 문안에
도달하지 못했고, 참조 두 장이 둘 다 몸이라 섞였다.

★**이 시험은 소스를 읽는다** — 두 경로가 *같은 자리에서* 같은 판단을
 하는지(배치에만 있는 수리가 또 생기지 않는지)를 보는 용도다.

★★**「함수를 태울 수 없다」고 적었던 첫 머리말은 틀렸다**(Codex 감사).
 생성 경계와 DB·파일을 대역으로 두면 무료로 끝까지 돈다 —
 `test_manual_composite_flow.py` 가 그것이고, **그쪽이 본 시험**이다.
 여기 AST 검사는 조건을 거꾸로 써도 통과할 수 있다(실측: 조건을 뒤집자
 AST 는 1건, 흐름 시험은 3건을 잡았다).
"""
from __future__ import annotations

import ast
import inspect
import pathlib

import pytest

from app.services import reference_composite_service as manual_mod
from app.services import reference_phase3_service as batch_mod


def _src(mod) -> str:
    return pathlib.Path(inspect.getfile(mod)).read_text(encoding="utf-8")


MANUAL = _src(manual_mod)
BATCH = _src(batch_mod)


def _fn(src: str, name: str) -> ast.AST:
    tree = ast.parse(src)
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if node.name == name:
                return node
    raise AssertionError(f"{name} 을 못 찾았다")


def test_manual_decides_derive_before_requiring_the_outfit_photo():
    """판정이 **옷 사진 필수 조건보다 앞**에 있어야 한다.

    반례(이 시험이 막는 것): derive 여부를 알기 전에 옷 사진을 필수로
    읽으면, 쓰지도 않을 것이 없다고 로봇의 수동 합성을 막는다.
    """
    fn = _fn(MANUAL, "generate_composite_image")
    body = ast.unparse(fn)
    i_derive = body.find("body_identity_short_ids")
    i_outfit = body.find("image.outfit_missing")
    assert i_derive > 0 and i_outfit > 0, "전제 확인 — 두 조각이 다 있어야 한다"
    assert i_derive < i_outfit, (
        "옷 사진 필수 조건이 derive 판정보다 먼저다 — 로봇이 막힌다")


def test_manual_skips_the_outfit_photo_for_body_identity():
    """derive 면 옷 사진을 **참조에 안 붙인다** — 배치와 같은 계약."""
    fn = _fn(MANUAL, "generate_composite_image")
    body = ast.unparse(fn)
    i_refs = body.find("Outfit piece")
    assert i_refs > 0, "전제 확인"
    # 그 append 가 `not _derive` 아래에 있는지 — 조건 없이 붙으면 실패
    guarded = "if not _derive" in body[:i_refs][-400:]
    assert guarded, "옷 사진이 derive 에서도 무조건 붙는다"


def test_manual_passes_the_real_outfit_description():
    """옷 **상세**를 넘긴다 — 이름 한 줄이 아니다.

    배치는 `outlook_description=task.get("outlook_description")` 을 넘긴다.
    수동이 안 넘기면 `ref_image_pipeline` 이 `entity_description`
    (=`<인물> wearing <옷 이름>`)으로 대체해, 모자·장화가 사라진다.
    """
    fn = _fn(MANUAL, "generate_composite_image")
    body = ast.unparse(fn)
    assert "outlook_description=" in body, "옷 상세를 안 넘긴다"
    i = body.find("outlook_description=")
    seg = body[i:i + 120]
    assert "outlook_ent.description" in seg, (
        f"옷 상세의 출처가 정본 설명이 아니다: {seg[:80]!r}")


def test_manual_lineage_drops_the_unused_outfit():
    """안 쓴 입력은 **계보에도 안 남긴다** — 「무엇을 보고 그렸나」가 거짓이 된다."""
    fn = _fn(MANUAL, "generate_composite_image")
    body = ast.unparse(fn)
    i = body.find("input_image_ids")
    assert i > 0, "전제 확인"
    seg = body[i:i + 260]
    assert "_derive" in seg, "파생 갈래에서도 옷 자산을 계보에 넣는다"


@pytest.mark.parametrize("marker", [
    "body_identity_short_ids",     # 같은 판정을 읽는다
    "composite_derive",            # 같은 타입으로 문안을 가른다
])
def test_both_paths_share_the_same_switch(marker):
    """배치와 수동이 **같은 판정·같은 타입**을 쓴다."""
    assert marker in MANUAL, f"수동에 {marker} 가 없다"
    assert (marker in BATCH
            or marker.replace("_short_ids", "") in BATCH), (
        f"배치에 {marker} 가 없다 — 전제가 깨졌다")


def test_entity_regeneration_goes_through_the_fixed_path():
    """엔티티별 재생성이 **이 함수를 거친다** — 따로 조립하면 수리가 또 갈린다."""
    fn = _fn(MANUAL, "regenerate_composites_for_entity")
    body = ast.unparse(fn)
    assert "self.generate_composite_image(" in body, (
        "재생성이 수리된 경로를 안 거친다")


# ── 검사 입력 — 옷 상세가 검사에도 간다 (2026-09-20 Codex 감사 2번) ──

def test_validation_description_carries_the_outfit_detail():
    """합성 검사가 **이름 한 줄**만 보던 것.

    생성 문안은 `outlook_description`(옷의 실제 상세)을 받는데 검사는
    `entity_description`(=`<인물> wearing <옷 이름>`)만 받았다. 그래서
    「커다란 밀짚모자와 거대한 장화」가 빠진 그림도 검사를 통과했다 —
    검사가 그런 요구가 있는 줄 몰랐다.

    ★새 유료 검사를 더하는 것이 아니라 **같은 호출에 요구사항을 싣는다.**
    """
    from app.modules.pipeline.ref_image_pipeline import _validation_description

    base = "찰리 wearing 알록달록우비세트"
    detail = "커다란 밀짚모자와 거대한 장화, 알록달록한 우비"
    out = _validation_description(base, detail)
    assert base in out, "인물·옷 이름이 사라졌다"
    assert "밀짚모자" in out and "장화" in out, "옷 상세가 검사에 안 간다"


def test_validation_description_is_unchanged_without_detail():
    """상세가 없거나 같으면 **종전 그대로** — 기존 소비자 보호."""
    from app.modules.pipeline.ref_image_pipeline import _validation_description

    base = "낡은 트럭"
    assert _validation_description(base, None) == base
    assert _validation_description(base, "") == base
    assert _validation_description(base, base) == base


def test_the_validation_call_uses_it():
    """조립부가 아니라 **검사 호출**이 그 값을 쓰는지 본다."""
    import inspect
    import pathlib

    from app.modules.pipeline import ref_image_pipeline as mod

    src = pathlib.Path(inspect.getfile(mod)).read_text(encoding="utf-8")
    i = src.find("validation = validate_reference_image(")
    assert i > 0, "전제 확인"
    seg = src[i:i + 260]
    # 합친 설명은 **한 번 만들어** 검사·비교가 함께 쓴다(BLOCK 2 수리 뒤)
    assert "_checked_description" in seg, (
        "검사 호출이 여전히 이름 한 줄만 넘긴다")
