"""outdoor_structure_seed 결정론 테스트 — 프롬프트 조립만.

레인2 재설계(2026-07-15 사용자 지시): 구조물 seed 순수 T2I → seed 기반
항공뷰 맵 → 이후 레인1(마커 맵) 파이프. 생성 완성도는 canary 육안.
"""
import pytest

from app.modules.pipeline.outdoor_structure_seed import (
    build_aerial_map_prompt,
    build_structure_seed_prompt,
    resolve_seed_pack_version,
)


def _norm(text: str) -> str:
    """md 줄바꿈이 계약 문구를 쪼개므로 공백 정규화 후 substring 비교."""
    return " ".join(text.lower().split())


def test_seed_prompt_assembles_scale_and_look_contract():
    p = build_structure_seed_prompt(
        structure_desc="낡은 다세대 빌라의 콘크리트 옥상 평면과 외벽 구조",
        interior_note_en="a living room, two bedrooms and a bathroom",
        world_anchor=" — contemporary SAMPLE region, 2026",
        place_text="EXT. SAMPLE 주택 - 낮",
    )
    assert "낡은 다세대 빌라의 콘크리트 옥상 평면과 외벽 구조" in p
    assert "a living room, two bedrooms and a bathroom" in p
    assert p.count("SCALE SPEC") == 1
    assert "contemporary SAMPLE region" in p
    assert "EXT. SAMPLE 주택 - 낮" in p
    assert "no people" in p
    # 순수 T2I — 참조/씨드 언급 없음 (참조 이미지가 없는 생성)
    assert "attached" not in _norm(p)
    # v2 (Codex B1): INTERIOR=size-only evidence — 실내 시각화 금지 봉인
    assert "size-only" in p
    assert "NEVER visualize" in p
    # v3 (재리뷰): cutaway 방어=의미 한정 — 절대 'no open walls' 금지 문구
    # 는 개방형 고정 구조물과 모순되므로 제거
    assert "transparent" in p     # enclosed 표면 투명화 금지 (의미 한정형)
    assert "intentional open sides" in _norm(p) or \
        "intentional open" in _norm(p)
    assert "no transparent or open walls" not in _norm(p)
    # v3: 시간 불변=target-relative — transient 배제하되 target 자체 예외
    assert "transient" in _norm(p)
    assert "does not remove the target structure" in _norm(p)
    assert "no vehicles parked or moving" not in _norm(p)
    assert "typical of the region" not in p  # 무근거 창작 개방 문구 제거


def test_seed_prompt_target_relative_contracts():
    """개방형/이동수단형 target 과 모순되지 않는 계약 (재리뷰 NARROW_1).

    interior 없어도 시간 불변(target 예외 포함)·enclosed 투명화 금지
    계약은 항상 존재하되, target 을 지울 수 있는 절대 금지 문구는 없다.
    """
    for desc in ("a fixed open-sided structure",
                 "a permanently moored transport structure"):
        p = build_structure_seed_prompt(
            structure_desc=desc,
            interior_note_en="",
            world_anchor="",
            place_text="EXT. SAMPLE",
        )
        assert "does not remove the target structure" in _norm(p)
        assert "intentional open" in _norm(p)
        assert "no transparent or open walls" not in _norm(p)
        assert "no vehicles parked or moving" not in _norm(p)
        assert "no people" in _norm(p)  # 사람은 절대 배제 유지


def test_derive_seed_inputs_persistent_site_sot():
    """Stage D ⑥: structure_desc=spec 검증 서술 SOT + transient negative."""
    from app.modules.pipeline.outdoor_structure_seed import derive_seed_inputs

    spec = {
        "layout_narration_en": "A SAMPLE narrow path reaches the entrance.",
        "items": [
            {"code": "Y1", "name_en": "SAMPLE main building",
             "placement_en": "central mass", "temporal_scope":
             "persistent_site"},
            {"code": "S1", "name_en": "SAMPLE fixed stair",
             "placement_en": "", "temporal_scope": "persistent_site"},
        ],
        "excluded_transient_elements": [
            {"name_en": "SAMPLE hanging line", "reason_en": "momentary"},
            {"name_en": "SAMPLE movable platform", "reason_en": "movable"},
        ],
    }
    group = {"members": [
        {"loc_id": "L1", "is_indoor": True},
        {"loc_id": "L2", "is_indoor": False},
    ]}
    locs = {
        "L1": {"name": "SAMPLE 내부", "description": "거실과 방 두 개"},
        "L2": {"name": "SAMPLE 외부", "description": "외부 마당"},
    }
    out = derive_seed_inputs(
        spec=spec, building_group=group, locations_by_id=locs)
    assert "SAMPLE narrow path" in out["structure_desc"]
    assert "- SAMPLE main building: central mass" in out["structure_desc"]
    assert "- SAMPLE fixed stair" in out["structure_desc"]
    # raw entity_merge 실외 서술은 위상 SOT 아님 — structure_desc 미포함
    # (v4: 외관 look evidence 로만 exterior_note_en 에 병기)
    assert "외부 마당" not in out["structure_desc"]
    assert out["excluded_transient_en"] == (
        "SAMPLE hanging line; SAMPLE movable platform")
    assert "거실과 방 두 개" in out["interior_note_en"]
    assert "외부 마당" in out["exterior_note_en"]
    # 실내/실외 서술 교차 오염 없음
    assert "거실과 방 두 개" not in out["exterior_note_en"]
    assert "외부 마당" not in out["interior_note_en"]


def test_derive_seed_inputs_exterior_note_edge_cases():
    """v4: is_indoor 미상(None/비bool)=미분류 default-deny(양쪽 제외 —
    Codex NARROW-1: legacy CP 실내 서술의 EXTERIOR 주입 차단),
    명시 False=exterior 병기, 빈 서술=제외."""
    from app.modules.pipeline.outdoor_structure_seed import derive_seed_inputs

    spec = {"layout_narration_en": "SAMPLE layout.", "items": []}
    group = {"members": [
        {"loc_id": "L1"},                        # is_indoor 미상(None)
        {"loc_id": "L2", "is_indoor": False},    # 서술 공백
        {"loc_id": "L3", "is_indoor": False},    # 명시 실외
        {"loc_id": "L4", "is_indoor": "yes"},    # 비bool
    ]}
    locs = {
        "L1": {"name": "SAMPLE 미상", "description": "미분류 서술"},
        "L2": {"name": "SAMPLE 외부 B", "description": "  "},
        "L3": {"name": "SAMPLE 외부 C", "description": "낡은 외벽"},
        "L4": {"name": "SAMPLE 비불", "description": "비불 서술"},
    }
    out = derive_seed_inputs(
        spec=spec, building_group=group, locations_by_id=locs)
    assert "낡은 외벽" in out["exterior_note_en"]
    assert "SAMPLE 외부 B" not in out["exterior_note_en"]
    # 미분류(None/비bool)는 interior/exterior 어느 쪽에도 없음
    for bad in ("미분류 서술", "비불 서술"):
        assert bad not in out["exterior_note_en"]
        assert bad not in out["interior_note_en"]
    assert out["interior_note_en"] == ""


def test_seed_prompt_excluded_transient_negative_contract():
    """excluded_transient_en 有=negative 절, 빈 문자열=기존 조립 불변."""
    base = dict(
        structure_desc="SAMPLE 고정 구조물",
        interior_note_en="",
        world_anchor="",
        place_text="EXT. SAMPLE",
    )
    p = build_structure_seed_prompt(
        **base, excluded_transient_en="SAMPLE hanging line")
    n = " ".join(p.split())
    assert "EXCLUDED — TRANSIENT ELEMENTS" in n
    assert "SAMPLE hanging line" in n
    p_off = build_structure_seed_prompt(**base)
    assert "EXCLUDED — TRANSIENT ELEMENTS" not in p_off
    assert p_off == build_structure_seed_prompt(
        **base, excluded_transient_en="")


def test_seed_prompt_exterior_appearance_clause_v4():
    """v4: 실외 외관 서술=look evidence 절, 위상 권위·EXCLUDED 우선 명시."""
    base = dict(
        structure_desc="SAMPLE 고정 구조물",
        interior_note_en="",
        world_anchor="",
        place_text="EXT. SAMPLE",
    )
    p = build_structure_seed_prompt(
        **base, exterior_note_en="SAMPLE 외부: 낡은 외벽과 거친 마감")
    n = _norm(p)
    assert p.count("EXTERIOR APPEARANCE") == 1
    assert "낡은 외벽과 거친 마감" in p
    # 위상 SOT=THE STRUCTURE 단독 권위 계약
    assert "sole authority" in n
    # transient 는 EXCLUDED negative 우선 계약
    assert "exclusion wins" in n
    # 빈 문자열=절 생략 + 기존 조립 byte-identical
    p_off = build_structure_seed_prompt(**base)
    assert "EXTERIOR APPEARANCE" not in p_off
    assert p_off == build_structure_seed_prompt(**base, exterior_note_en="")


def test_seed_prompt_v3_with_exterior_note_fails_closed():
    """v4 미만 팩에 exterior 절 없음 — 조용한 누락 대신 명시 실패."""
    with pytest.raises(FileNotFoundError, match="exterior_clause"):
        build_structure_seed_prompt(
            structure_desc="SAMPLE 고정 구조물",
            exterior_note_en="SAMPLE 외관",
            prompt_version="3",
        )


def test_seed_prompt_scale_optional_no_dangling():
    """interior 없으면 SCALE SPEC 언급 0 (Stage C H2 교훈 선반영)."""
    p = build_structure_seed_prompt(
        structure_desc="SAMPLE 고정 구조물",
        interior_note_en="",
        world_anchor="",
        place_text="EXT. SAMPLE",
    )
    assert "SCALE SPEC" not in p
    assert "INTERIOR" not in p


def test_aerial_prompt_topdown_conversion_contract():
    p = build_aerial_map_prompt()
    # seed 참조 전제의 탑다운 변환 계약 + 마커/텍스트 금지
    assert "attached" in _norm(p)
    assert "top-down" in _norm(p) or "overhead" in _norm(p)
    assert "no text" in _norm(p) or "no letters" in _norm(p)
    assert "same" in _norm(p)  # 동일 구조물 유지
    # v2: 지붕·외관 온전(실내 노출 금지) + permanent 만 계승
    assert "intact" in _norm(p)
    assert "permanent" in _norm(p)
    assert "cutaway" in _norm(p)  # no cutaway 계약
    # v3: aerial 도 target 예외 유지 (변환 단계에서 target 제거 방지)
    assert "does not remove the target structure" in _norm(p)
    assert "no vehicles parked or moving" not in _norm(p)


def test_packs_are_scenario_neutral():
    """팩 파일에 구조물 종류 하드코딩 0 (Stage C BLOCKING 교훈 선반영)."""
    from pathlib import Path

    repo = Path(__file__).resolve().parents[3]
    for sel in ("1", "2", "3", "4"):
        ver = resolve_seed_pack_version(sel)
        for f in (repo / "prompts" / "_base" / "structure_seed" / ver).glob(
                "*.md"):
            text = f.read_text(encoding="utf-8").lower()
            for word in ("rooftop", "villa", "compact", "tiny"):
                assert word not in text, f"{f.name} 에 {word!r} 하드코딩"


def test_unknown_version_raises():
    with pytest.raises(ValueError):
        resolve_seed_pack_version("99")


# ── 2026-07-19 재설계 A: seed 플레이트 참조 (aerial 대체) ─────────────


def test_build_seed_plate_refs_maps_outdoor_locs(tmp_path):
    from app.modules.pipeline.outdoor_structure_seed import (
        build_seed_plate_refs,
    )

    seed_png = tmp_path / "seed_ok.png"
    seed_png.write_bytes(b"png")
    seed_groups = {
        "g_ok": {"status": "ok", "seed_png_path": str(seed_png)},
        "g_failed": {"status": "failed"},
        "g_missing_file": {"status": "ok",
                           "seed_png_path": str(tmp_path / "absent.png")},
    }
    # ★ E2E9 실측 회귀: background_classify 실제 member 키는 ``loc_id``.
    # (이전 fixture 가 location_id 로 작성돼 배선 버그(항상 빈 매핑 —
    # seed 미첨부)를 통과시켰다. legacy location_id 는 fallback 1건만.)
    building_groups = [
        {"group_id": "g_ok", "members": [
            {"loc_id": "L01", "is_indoor": False},
            {"loc_id": "L02", "is_indoor": True},   # 실내 제외
            {"location_id": "L05", "is_indoor": False},  # legacy fallback
        ]},
        {"group_id": "g_failed", "members": [
            {"loc_id": "L03", "is_indoor": False},
        ]},
        {"group_id": "g_missing_file", "members": [
            {"loc_id": "L04", "is_indoor": False},
        ]},
    ]
    out = build_seed_plate_refs(seed_groups, building_groups)
    assert set(out) == {"L01", "L05"}  # 야외+ok+파일 실존만
    assert out["L01"]["group_id"] == "g_ok"
    assert out["L01"]["path"] == str(seed_png)


def test_seed_site_guidance_neutral_and_no_copy_clause():
    from app.modules.pipeline.outdoor_structure_seed import (
        SEED_SITE_GUIDANCE,
    )

    low = SEED_SITE_GUIDANCE.lower()
    assert "do not copy" in low or "never copy" in low  # 카메라 복사 금지
    for banned in ("옥탑", "정류장", "금월"):
        assert banned not in SEED_SITE_GUIDANCE


def test_step_seed_plate_flag_default_off_and_wired():
    import inspect

    from app.core.config import settings
    from app.core.steps import background_render_step

    assert settings.outdoor_seed_plate_reference_enabled is False
    src = inspect.getsource(background_render_step)
    assert "outdoor_seed_plate_reference_enabled" in src
    assert "build_seed_plate_refs" in src
    assert "SEED_SITE_GUIDANCE" in src


def test_collect_seed_groups_all_groups_mode():
    """재설계 B-1: seed 일반화 — all_groups=True 면 status ok 인 야외
    전 그룹(lane 무관), False(기존)=structure_plate 그룹만."""
    from app.core.steps.outdoor_structure_seed_step import (
        collect_lane2_groups,
    )

    lane_data = {"groups": {
        "g_struct": {"status": "ok", "plan": {"shot_bindings": [
            {"lane": "structure_plate"}]}},
        "g_move": {"status": "ok", "plan": {"shot_bindings": [
            {"lane": "map_marker"}]}},
        "g_none": {"status": "ok", "plan": {"shot_bindings": [
            {"lane": "none"}]}},
        "g_failed": {"status": "failed"},
    }}
    assert collect_lane2_groups(lane_data) == ["g_struct"]
    assert collect_lane2_groups(lane_data, all_groups=True) == [
        "g_move", "g_none", "g_struct"]


def test_seed_all_groups_flag_default_off_and_wired():
    import inspect

    from app.core.config import settings
    from app.core.steps import outdoor_structure_seed_step

    assert settings.outdoor_seed_all_groups_enabled is False
    src = inspect.getsource(outdoor_structure_seed_step)
    assert "outdoor_seed_all_groups_enabled" in src


# ── 팩 v5 (E2E11 fix①): HUMAN-SCALE CALIBRATION 상시 주입 ─────────────


def test_v5_appends_human_scale_clause_always():
    from app.modules.pipeline.outdoor_structure_seed import (
        build_structure_seed_prompt,
    )

    # interior_note 결손(=기존 SCALE SPEC 절 생략 조건)에서도 앵커 실림
    p = build_structure_seed_prompt(
        structure_desc="a small rooftop room with one window",
        prompt_version="5",
    )
    assert "HUMAN-SCALE CALIBRATION" in p
    assert "STRUCTURE SCALE SPEC" not in p  # interior 없으면 기존 절은 생략


def test_v4_assembly_unchanged():
    from app.modules.pipeline.outdoor_structure_seed import (
        build_structure_seed_prompt,
    )

    p = build_structure_seed_prompt(
        structure_desc="a small rooftop room with one window",
        prompt_version="4",
    )
    assert "HUMAN-SCALE CALIBRATION" not in p
