"""W21B scale-guideline — floor_plan_prompt v10 pack + validator 안전망.

배경: 평면 탑다운 FP 에는 수직 규모 정보(층수·높이·개구부 수)가 없어,
하류가 FP 를 볼륨 스케치/입면/렌더로 변환할 때 건물이 전부 1층으로
붕괴하는 결함이 실험(s27 45° 스케치)에서 실증됨. v10 은 v6 기반 pack 에
필수 필드 ``scale_guideline`` (evidence-bound 최소 공간 규모 가이드)을
추가한다 — 기존 v6-family 필드/계약 무변경 (additive superset).

확인 계약:
1. v10 pack 세 stem (system.md / user_template.md / schema.json) 동반 존재.
2. v10 schema — ``scale_guideline`` required + minLength, v6 대비 additive
   superset (기존 property 무변경).
3. selector "10" versioned-load pin (auto-latest resolve 없음).
4. ``_validate_fp_prompt_extras`` 안전망 — selector "10" 에서
   scale_guideline 부재/短문 reject, 정상 통과. v6 selector 는 무영향.
5. 시나리오 specific token leak 없음.
LLM/이미지/DB 호출 0.
"""
from __future__ import annotations

import json

import pytest

from app.core.fp_prompt_compat import (
    SCALE_GUIDELINE_FP_PROMPT_VERSIONS,
    V6_COMPATIBLE_FP_PROMPT_VERSIONS,
)
from app.modules.pipeline.floor_plan_prompt import (
    PROMPT_VERSION_MAP,
    FloorPlanPromptError,
    _validate_fp_prompt_extras,
    resolve_prompt_version,
)
from app.modules.prompt_loader import PROMPTS_BASE, load_prompt, load_schema

V6_PACK_DIR = PROMPTS_BASE / "floor_plan_prompt" / PROMPT_VERSION_MAP["6"]
V10_PACK = PROMPT_VERSION_MAP["10"]
V10_PACK_DIR = PROMPTS_BASE / "floor_plan_prompt" / V10_PACK


def _v10_shape_payload(*, guideline: object = None) -> dict:
    """v6 valid shape + scale_guideline (None 이면 필드 자체 누락)."""
    payload = {
        "fp_id": "fp_a",
        "t2i_prompt": "x" * 50,
        "key_elements": [],
        "numbered_elements": [
            {
                "number": 1,
                "label": "primary unit",
                "category": "area",
                "position_hint": "center area",
                "base_layer_decision": "base_structural_unit",
            },
        ],
        "camera_recommendations": [
            {
                "bg_id": "L01B01",
                "sub_location": "primary_unit",
                "camera_position": "near number 1, facing across the unit",
                "camera_height": "eye level",
                "lens_hint": "35mm",
                "framing_notes": "",
                "use_numbered_elements": [1],
                "ignore_numbered_elements": [],
            }
        ],
    }
    if guideline is not None:
        payload["scale_guideline"] = guideline
    return payload


# ----- 1. pack integrity -----


def test_v10_pack_has_all_three_stems():
    for stem in ("system.md", "user_template.md", "schema.json"):
        assert (V10_PACK_DIR / stem).exists(), f"v10 pack missing {stem}"


def test_v10_selector_resolves_to_pinned_pack():
    assert resolve_prompt_version("10") == V10_PACK
    assert V10_PACK.startswith("10.")


def test_v10_in_compat_sets():
    assert "10" in V6_COMPATIBLE_FP_PROMPT_VERSIONS
    # v11 (2026-07-17): v10 schema byte-identical — scale_guideline carry
    assert SCALE_GUIDELINE_FP_PROMPT_VERSIONS == frozenset({"10", "11"})


# ----- 2. schema delta -----


def _schema(pack_dir) -> dict:
    return json.loads((pack_dir / "schema.json").read_text(encoding="utf-8"))


def test_v10_schema_requires_scale_guideline_with_min_length():
    s = _schema(V10_PACK_DIR)
    assert "scale_guideline" in s["required"]
    prop = s["properties"]["scale_guideline"]
    assert prop["type"] == "string"
    assert prop["minLength"] >= 30


def test_v10_schema_is_additive_superset_of_v6():
    """v6 가 정의한 property/required 는 v10 에서 무변경 — 추가만 허용."""
    v6, v10 = _schema(V6_PACK_DIR), _schema(V10_PACK_DIR)
    for key, spec in v6["properties"].items():
        assert v10["properties"][key] == spec, f"v10 changed v6 property {key}"
    assert set(v10["required"]) == set(v6["required"]) | {"scale_guideline"}
    assert v10["additionalProperties"] is False


def test_v10_user_template_identical_to_v6():
    assert (V10_PACK_DIR / "user_template.md").read_bytes() == (
        V6_PACK_DIR / "user_template.md"
    ).read_bytes()


# ----- 3. versioned load pin -----


def test_versioned_load_prompt_pins_v10_pack():
    text = load_prompt("floor_plan_prompt", "system", version=V10_PACK)
    assert "scale_guideline" in text
    assert "metadata" in text.lower()


def test_versioned_load_schema_pins_v10_pack():
    s = load_schema("floor_plan_prompt", "schema", version=V10_PACK)
    assert "scale_guideline" in s["required"]


# ----- 4. validator safety net -----


def test_validator_v10_rejects_missing_scale_guideline():
    with pytest.raises(FloorPlanPromptError, match="scale_guideline"):
        _validate_fp_prompt_extras(
            fp_result=_v10_shape_payload(),
            expected_bg_ids={"L01B01"},
            prompt_version="10",
        )


def test_validator_v10_rejects_short_scale_guideline():
    with pytest.raises(FloorPlanPromptError, match="scale_guideline"):
        _validate_fp_prompt_extras(
            fp_result=_v10_shape_payload(guideline="too short"),
            expected_bg_ids={"L01B01"},
            prompt_version="10",
        )


def test_validator_v10_rejects_non_string_scale_guideline():
    with pytest.raises(FloorPlanPromptError, match="scale_guideline"):
        _validate_fp_prompt_extras(
            fp_result=_v10_shape_payload(guideline=["not", "a", "string"]),
            expected_bg_ids={"L01B01"},
            prompt_version="10",
        )


def test_validator_v10_accepts_valid_scale_guideline():
    _validate_fp_prompt_extras(
        fp_result=_v10_shape_payload(
            guideline=(
                "The main building is a three-storey multi-family unit, "
                "roughly 8-10 m to its roof deck; the exterior stair runs "
                "the full three storeys. Storey count typical for the "
                "region; not specified in the text."
            )
        ),
        expected_bg_ids={"L01B01"},
        prompt_version="10",
    )


def test_validator_v6_selector_unaffected_by_missing_scale_guideline():
    """v6 selector 는 scale_guideline 부재여도 기존 동작 verbatim."""
    _validate_fp_prompt_extras(
        fp_result=_v10_shape_payload(),
        expected_bg_ids={"L01B01"},
        prompt_version="6",
    )


# ----- 5. creator corrections injection (wave3 4번째 사이트) -----


def _capture_call(captured):
    def fake_call_structured(**kwargs):
        captured.update(kwargs)
        return _v10_shape_payload(
            guideline="A three-storey building with a small rooftop unit; "
                      "typical storey heights, not text-specified.")
    return fake_call_structured


def test_run_appends_corrections_block_to_system():
    from app.modules.pipeline.floor_plan_prompt import run_floor_plan_prompt
    captured: dict = {}
    block = "\n\n## CREATOR CORRECTIONS\n- the stair is a steel retrofit stair"
    run_floor_plan_prompt(
        user_prompt="u", expected_fp_id="fp_a",
        call_structured_fn=_capture_call(captured),
        expected_bg_ids={"L01B01"}, prompt_version="10",
        creator_corrections_block=block,
    )
    assert captured["system_prompt"].endswith(block)


def test_run_without_corrections_block_is_byte_identical_system():
    from app.modules.pipeline.floor_plan_prompt import run_floor_plan_prompt
    a: dict = {}
    b: dict = {}
    for cap, kw in ((a, {}), (b, {"creator_corrections_block": ""})):
        run_floor_plan_prompt(
            user_prompt="u", expected_fp_id="fp_a",
            call_structured_fn=_capture_call(cap),
            expected_bg_ids={"L01B01"}, prompt_version="10", **kw,
        )
    assert a["system_prompt"] == b["system_prompt"]


# ----- 6. scenario hygiene -----


def test_v10_pack_has_no_scenario_specific_tokens():
    banned = {
        "옥탑방", "옥탑", "민숙", "수리영", "suri-young", "suri young",
        "금월도", "geumwol", "l05b", "l05_", "l07b",
        "corpse", "cadaver", "body bag",
        "빌라", "villa",  # 이번 결함 사례의 건물 유형 — 팩은 유형 중립 유지
    }
    for stem in ("system.md", "user_template.md"):
        text = (V10_PACK_DIR / stem).read_text(encoding="utf-8").lower()
        for token in banned:
            assert token not in text, f"{stem} leaked token {token!r}"
