"""outdoor_shot_grounding 모듈 결정론 테스트 (W22 ③).

동적 enum 스키마/블록 조립/ID-free 검증/재시도 루프만 — VLM 완성도는
검증하지 않는다. fixture 전부 시나리오 중립 SAMPLE.
"""

import pytest

from app.core.errors import AppError
from app.modules.pipeline.outdoor_direct_common import (
    build_selected_keys,
    filter_group_shots,
)
from app.modules.pipeline.outdoor_shot_grounding import (
    build_ground_schema,
    build_legend_block,
    build_shot_block,
    build_user_parts,
    resolve_prompt_version,
    run_outdoor_shot_grounding_shot,
    validate_ground,
)


def _spec():
    return {
        "layout_narration_en": "A walled yard before the main structure.",
        "zone_labels_en": ["Front Yard", "Rear Path"],
        "items": [
            {"code": "P1", "kind": "gate", "name_en": "front entry gate",
             "placement_en": "south wall of the yard",
             "inferred": False,
             "evidence": {"scene_index": 3, "quote_ko": "대문"}},
            {"code": "Q1", "kind": "approach", "name_en": "rear service path",
             "placement_en": "west side toward the rear",
             "inferred": True, "evidence": None},
        ],
    }


def _ground(**over):
    base = {
        "map_zone": "Front Yard",
        "anchor_markers": ["P1"],
        "camera_position_en": "just inside the front entry gate",
        "look_direction_en": "toward the main structure",
        "in_frame_en": "gate frame near, yard middle, structure far",
        "rationale_ko": "대문 진입 순간이라 마당 존이 맞다",
        "moment_context_en": "",
    }
    base.update(over)
    return base


# ── 스키마/블록 ──────────────────────────────────────────────────────


def test_resolve_prompt_version():
    assert resolve_prompt_version("1")
    with pytest.raises(ValueError):
        resolve_prompt_version("99")


def test_ground_schema_has_dynamic_enums():
    schema = build_ground_schema(_spec())
    assert schema["properties"]["map_zone"]["enum"] == ["Front Yard", "Rear Path"]
    assert schema["properties"]["anchor_markers"]["items"]["enum"] == ["P1", "Q1"]
    assert "moment_context_en" in schema["required"]


def test_ground_schema_rejects_empty_spec():
    with pytest.raises(AppError):
        build_ground_schema({"zone_labels_en": [], "items": []})


def test_legend_block_contains_codes():
    block = build_legend_block(_spec())
    assert "(P1)" in block and "front entry gate" in block


def test_shot_block_includes_staging_hints():
    block = build_shot_block({
        "scene_index": 3, "shot_index": 1,
        "description": "대문으로 들어서는 인물",
        "characters": ["인물A"],
        "camera_direction": "정면 로우앵글",
        "key_bg_elements": [{"element": "담"}],
    })
    assert "S3_Shot1" in block and "정면 로우앵글" in block and "담" in block


def test_user_parts_order_and_full_scene_text():
    long_text = "긴 씬 원문. " * 3000
    parts = build_user_parts(
        "{legend_block}|{zones_block}|{shot_block}|{scene_text_block}",
        spec=_spec(),
        shot={"scene_index": 3, "shot_index": 1, "description": "샷"},
        scene_text=long_text,
        map_png=b"PNG",
    )
    assert parts[0]["type"] == "text" and "SITE PLAN" in parts[0]["text"]
    assert parts[1]["type"] == "image_url"
    assert long_text in parts[2]["text"]  # 전문 보존


# ── ID-free 검증 ─────────────────────────────────────────────────────


def test_validate_ground_passes_clean():
    assert validate_ground(_ground(), ["P1", "Q1"]) == []


def test_validate_ground_rejects_code_in_prose():
    bad = _ground(camera_position_en="standing at P1 looking north")
    violations = validate_ground(bad, ["P1", "Q1"])
    assert any("camera_position_en" in v for v in violations)


def test_validate_ground_rejects_code_in_moment_context():
    bad = _ground(moment_context_en="the vehicle near Q1 is a patrol car")
    violations = validate_ground(bad, ["P1", "Q1"])
    assert any("moment_context_en" in v for v in violations)


def test_validate_ground_word_boundary_no_false_positive():
    ok = _ground(in_frame_en="lot P10 signage removed, yard, structure")
    assert validate_ground(ok, ["P1", "Q1"]) == []


# ── 재시도 루프 ──────────────────────────────────────────────────────


def _run_kwargs(fake_fn):
    return dict(
        spec=_spec(),
        shot={"scene_index": 3, "shot_index": 1,
              "description": "대문으로 들어서는 인물"},
        scene_text="그가 대문을 밀고 들어선다.",
        map_png=b"PNG",
        call_structured_fn=fake_fn,
    )


def test_run_ok_first_attempt_and_injects_corrections():
    captured = {}

    def fake(step, system, user, schema, **kw):
        captured["system"] = system
        captured["user"] = user
        captured["schema"] = schema
        return _ground()

    kwargs = _run_kwargs(fake)
    kwargs["creator_corrections_block"] = "\n\n## CREATOR CORRECTIONS\n- SAMPLE"
    out = run_outdoor_shot_grounding_shot(**kwargs)
    assert out["attempts"] == 1
    assert captured["system"].endswith("- SAMPLE")
    # 동적 enum 스키마 전달 확인
    assert captured["schema"]["properties"]["map_zone"]["enum"][0] == "Front Yard"
    # 멀티모달: 맵 이미지 파트 포함
    assert any(p.get("type") == "image_url" for p in captured["user"])


def test_run_retries_on_prose_code_leak():
    responses = [
        _ground(look_direction_en="toward P1"),
        _ground(),
    ]
    calls = []

    def fake(step, system, user, schema, **kw):
        calls.append(user)
        return responses[len(calls) - 1]

    out = run_outdoor_shot_grounding_shot(**_run_kwargs(fake))
    assert out["attempts"] == 2
    assert "재시도" in calls[1][-1]["text"]


def test_run_exhausts_raises():
    def fake(step, system, user, schema, **kw):
        return _ground(in_frame_en="P1 in frame")

    with pytest.raises(AppError) as ei:
        run_outdoor_shot_grounding_shot(**_run_kwargs(fake), max_attempts=2)
    assert ei.value.code == "step.contract_violation.outdoor_shot_grounding"


# ── 공용 헬퍼 (outdoor_direct_common) ────────────────────────────────


def test_build_selected_keys_none_when_missing():
    assert build_selected_keys(None) is None
    assert build_selected_keys({"data": {"scenes": []}}) is None


def test_filter_group_shots_respects_selection_and_loc():
    staging = [
        {"scene_index": 3, "shot_index": 1, "description": "그룹 loc 샷"},
        {"scene_index": 3, "shot_index": 2, "description": "타 loc 샷"},
        {"scene_index": 3, "shot_index": 4, "description": "미선택 잔존"},
    ]
    out = filter_group_shots(
        staging,
        scene_indices=[3],
        loc_ids={"L01"},
        scene_primary={3: "L01"},
        shot_loc_by_key={(3, 1): "L01", (3, 2): "L02", (3, 4): "L01"},
        selected_keys={(3, 1), (3, 2)},
    )
    assert [s["shot_index"] for s in out] == [1]
