"""OutdoorShotGroundingStep 결정론 테스트 (W22 ③) — 게이트/대상 선별/격리."""

from unittest.mock import MagicMock, patch

from app.core.steps.outdoor_shot_grounding_step import (
    SCHEMA_VERSION,
    OutdoorShotGroundingStep,
)


def _make_step():
    step = OutdoorShotGroundingStep.__new__(OutdoorShotGroundingStep)
    step.project_id = "SAMPLE_PROJECT"
    step.episode_id = "SAMPLE_EPISODE"
    step.project_config = {}
    step.build_opik_metadata = MagicMock(return_value={})
    return step


def _patch_settings(mode="on", flag=True):
    return (
        patch("app.core.config.settings.background_mode", mode),
        patch("app.core.config.settings.outdoor_direct_compose_enabled",
              flag, create=True),
    )


def _cps(tmp_path):
    map_png = tmp_path / "canon_g1_map.png"
    map_png.write_bytes(b"MAP")
    return {
        "outdoor_place_spec": {"data": {"groups": {
            "g1": {"spec": {"zone_labels_en": ["Front Yard"],
                            "items": [{"code": "P1", "name_en": "gate",
                                       "placement_en": "south wall",
                                       "kind": "gate", "inferred": True,
                                       "evidence": None}]},
                   "outdoor_loc_ids": ["L01"], "scene_indices": [3]},
            "g_nocanon": {"spec": {"zone_labels_en": ["Z"],
                                   "items": [{"code": "A1", "name_en": "x",
                                              "placement_en": "y",
                                              "kind": "k", "inferred": True,
                                              "evidence": None}]},
                          "outdoor_loc_ids": ["L05"], "scene_indices": [9]},
        }}},
        "outdoor_place_canon": {"data": {"groups": {
            "g1": {"status": "ok", "map_png_path": str(map_png),
                   "map_asset_id": "map-asset-1"},
            "g_nocanon": {"status": "failed", "error": "x"},
        }}},
        "scene_save": {"data": {"segments": [
            {"scene_index": 3, "text": "그가 대문을 밀고 들어선다. " * 300},
        ]}},
        "scene_director": {"data": {"scenes": [
            {"scene_index": 3, "primary_location": "L01"},
        ]}},
        "shot_validator": {"data": {"scenes": [
            {"scene_index": 3, "shots": [
                {"shot_index": 1, "location_id": "L01"},
                {"shot_index": 2, "location_id": "L02"},
            ]},
        ]}},
        "shot_selection": {"data": {"scenes": [
            {"scene_index": 3, "selected_shot_indices": [1, 2]},
        ]}},
        "shot_staging": {"data": {"shots": [
            {"scene_index": 3, "shot_index": 1, "description": "야외 샷"},
            {"scene_index": 3, "shot_index": 2, "description": "타 loc 샷"},
        ]}},
    }


def test_gate_flag_off():
    p1, p2 = _patch_settings(flag=False)
    with p1, p2:
        result = _make_step()._execute()
    assert result["applicable_count"] == 0
    assert result["schema_version"] == SCHEMA_VERSION


def test_grounds_group_shots_and_skips_canonless(tmp_path):
    step = _make_step()
    cps = _cps(tmp_path)
    step._load_prev_checkpoint = lambda sid: cps.get(sid)

    captured = []

    def fake_run(**kwargs):
        captured.append(kwargs)
        return {"ground": {"map_zone": "Front Yard"}, "attempts": 1}

    p1, p2 = _patch_settings()
    with p1, p2, patch(
        "app.modules.pipeline.outdoor_shot_grounding."
        "run_outdoor_shot_grounding_shot",
        side_effect=fake_run,
    ), patch(
        "app.core.creator_corrections.project_corrections_block",
        return_value="",
    ):
        result = step._execute()

    # g1 야외 샷 1개만 (같은 씬 타 loc 샷 제외), 캐논 결측 그룹은 skip
    assert result["applicable_count"] == 1
    assert result["completed_count"] == 1 and result["failed_count"] == 0
    g1 = result["data"]["groups"]["g1"]
    assert g1["map_asset_id"] == "map-asset-1"
    assert set(g1["shots"]) == {"3_1"}
    assert g1["shots"]["3_1"]["status"] == "ok"
    assert result["data"]["groups"]["g_nocanon"] == {
        "skipped": "canon assets missing"
    }
    # 씬 원문 전문 + 맵 bytes 전달 확인
    assert captured[0]["scene_text"].startswith("그가 대문을")
    assert len(captured[0]["scene_text"]) > 3000
    assert captured[0]["map_png"] == b"MAP"


def test_shot_failure_isolated(tmp_path):
    step = _make_step()
    cps = _cps(tmp_path)
    step._load_prev_checkpoint = lambda sid: cps.get(sid)

    def fake_run(**kwargs):
        raise RuntimeError("SAMPLE grounding 실패")

    p1, p2 = _patch_settings()
    with p1, p2, patch(
        "app.modules.pipeline.outdoor_shot_grounding."
        "run_outdoor_shot_grounding_shot",
        side_effect=fake_run,
    ), patch(
        "app.core.creator_corrections.project_corrections_block",
        return_value="",
    ):
        result = step._execute()

    assert result["failed_count"] == 1 and result["completed_count"] == 0
    assert result["data"]["groups"]["g1"]["shots"]["3_1"]["status"] == "failed"
