"""step_catalog — manifest + class 병합 View 검증.

Phase 1.1.
"""
import pytest

from app.core.step_catalog import (
    STEP_CATALOG, StepEntry,
    get_active_entries, get_all_downstream_recursive, get_depends_on,
    get_downstream_steps, get_entry, get_modifiers_of, get_ordered_entries,
    get_resume_sensitive_step_ids,
)
from app.core.step_manifest import STEP_MANIFEST


def test_catalog_has_all_manifest_steps():
    assert set(STEP_CATALOG.keys()) == set(STEP_MANIFEST.keys())


def test_catalog_entries_are_frozen():
    entry = next(iter(STEP_CATALOG.values()))
    with pytest.raises((AttributeError, Exception)):
        entry.label = "mutated"  # type: ignore[misc]


def test_runner_cls_bound_for_active_autorun_steps():
    """active + 자동 실행 경로(always/if_*)에 포함되는 step은 runner_cls 필수.

    on_demand/disabled는 수동 트리거이므로 runner 없어도 파이프라인 가동에는 영향 없음.
    """
    autorun_apps = {"always", "if_planning_doc", "if_has_outlooks"}
    unbound = [
        sid for sid, e in STEP_CATALOG.items()
        if e.lifecycle == "active"
        and e.applicability in autorun_apps
        and e.runner_cls is None
    ]
    assert not unbound, f"autorun active without runner_cls: {unbound}"


def test_get_depends_on_matches_manifest():
    for sid in STEP_MANIFEST:
        assert get_depends_on(sid) == list(STEP_MANIFEST[sid].get("depends_on", []))


def test_get_downstream_steps_matches_manifest():
    from app.core.step_manifest import get_downstream_steps as manifest_fn
    for sid in STEP_MANIFEST:
        assert sorted(get_downstream_steps(sid)) == sorted(manifest_fn(sid))


def test_get_all_downstream_recursive_matches_manifest():
    from app.core.step_manifest import get_all_downstream_recursive as manifest_fn
    for sid in STEP_MANIFEST:
        assert get_all_downstream_recursive(sid) == manifest_fn(sid)


def test_ordered_entries_monotonic():
    ordered = get_ordered_entries()
    for i in range(len(ordered) - 1):
        assert ordered[i].order <= ordered[i + 1].order


def test_resume_sensitive_step_ids_exact():
    """shot-more 신규 3 step (현재 규칙)."""
    assert set(get_resume_sensitive_step_ids()) == {
        "scene_camera_flow", "shot_staging", "scene_consistency",
    }


def test_get_active_entries_count():
    active = get_active_entries()
    # lifecycle=active: 51 (total 62 - deprecated 10 - removed 1).
    # Phase 7 +6 active (background_classify/master_plan/fp_prompt+render/bg_prompt+render).
    # Phase 5 -4 active (location_floor_plan/background_planner/background_chain_planning/background_chain_render
    # → all deprecated). Net +2 active. 2026-04-29 갱신.
    # reference-necessity Phase 2 +1 active (episode_reference_policy). 2026-05-23 갱신.
    # W19B-1/4 +1 active (floor_plan_overlay_payload). 2026-05-26 갱신.
    # W20A +1 active (base_location_dossier, opt-in / default OFF). 2026-05-27 갱신.
    # W20A2 +1 active (floor_plan_geometry_readback, opt-in / default OFF). 2026-05-27 갱신.
    # W20B +1 active (shot_aware_bg_render_plan, opt-in / default OFF). 2026-05-27 갱신.
    # W21B-w4 +1 active (floor_plan_semantic_readback, opt-in / default OFF). 2026-05-29 갱신.
    # W21B-w4 C2 +1 active (shot_projection_card, opt-in / default OFF). 2026-05-30 갱신.
    # W21B-w5 +2 active (bg_space_partition / floor_plan_light_sidecar, opt-in /
    # default OFF). 2026-05-31 — catch-up: these were not recorded here at the time.
    # W21B +1 active (dwelling_zone_map, opt-in / default OFF). 2026-06-08 갱신.
    # W21B-W8 +1 active (outdoor_site_layout, opt-in / default OFF). 2026-06-12 갱신.
    assert len(active) == 62


def test_get_active_entries_filtered_by_category():
    image_active = get_active_entries(category="image")
    assert all(e.category == "image" for e in image_active)
    # 5 always/if_has_outlooks + Phase 7 2 if_background_mode (floor_plan_render, background_render).
    # Phase 5 background_chain_render/location_floor_plan deprecated (제외). 2026-04-29 갱신.
    # W21B-w5 +1 image (floor_plan_light_sidecar, opt-in / default OFF). 2026-05-31 —
    # catch-up: not recorded here at the time (dwelling_zone_map is analysis, not image).
    # W21B fresh-E2E fix +1 (space_set_bg analysis→image 이동, 2026-06-10) → 9.
    assert len(image_active) == 9


def test_get_modifiers_of_t2i_review_targets():
    """t2i_review는 entity_t2i, scene_detail을 수정한다고 선언."""
    assert "t2i_review" in get_modifiers_of("entity_t2i")
    assert "t2i_review" in get_modifiers_of("scene_detail")


def test_get_modifiers_of_independent_step_empty():
    """아무도 수정하지 않는 step은 빈 리스트."""
    # text_cleanup은 editorial의 modifies 대상 아님
    assert get_modifiers_of("text_cleanup") == []


def test_get_entry_returns_frozen_entry():
    entry = get_entry("scene_detail")
    assert entry is not None
    assert isinstance(entry, StepEntry)
    assert entry.step_type == "transform"
    assert entry.lifecycle == "active"


# ----- W19B-1/4 manifest registration regression gates -----


def test_floor_plan_overlay_payload_registered_in_catalog():
    """W19B-1 producer step 이 catalog 에 등재되어 있어 pipeline ordering
    /applicability 가 이를 인식하는지 가드 (manifest 누락 회귀 방지)."""
    entry = get_entry("floor_plan_overlay_payload")
    assert entry is not None
    assert entry.lifecycle == "active"
    assert entry.applicability == "if_background_mode"
    assert entry.category == "analysis"
    assert entry.step_type == "transform"
    # runner_cls 도 자동 바인딩 — STEP_CLASSES 매핑 검증.
    from app.core.steps.floor_plan_overlay_payload_step import (
        FloorPlanOverlayPayloadStep,
    )
    assert entry.runner_cls is FloorPlanOverlayPayloadStep


def test_floor_plan_overlay_payload_order_between_render_and_bg_prompt():
    """ordering: floor_plan_render < floor_plan_overlay_payload < background_prompt.
    overlay payload 가 BG prompt 직전 단계로 들어가야 W19B-2 v7 opt-in 시
    cp 자동 생성."""
    overlay_entry = get_entry("floor_plan_overlay_payload")
    fp_render_entry = get_entry("floor_plan_render")
    bg_prompt_entry = get_entry("background_prompt")
    assert overlay_entry is not None
    assert fp_render_entry is not None
    assert bg_prompt_entry is not None
    assert fp_render_entry.order < overlay_entry.order < bg_prompt_entry.order


def test_background_prompt_depends_on_overlay_producer():
    """W19B-2 v7 path 의 required producer 가 manifest dependency 에 명시."""
    deps = get_depends_on("background_prompt")
    assert "floor_plan_overlay_payload" in deps
    assert "background_master_plan" in deps  # 기존 의존성 보존.
    assert "floor_plan_render" in deps


def test_background_render_depends_on_overlay_producer():
    """W19B-3 / W20E7-A: the w18j_overlap opt-in path historically read
    the overlay cp directly so the dependency stayed declared in the
    manifest; the dependency stays declared after W20E7-A deprecation
    (still consumed via diagnostic / future cleanup) until the cleanup
    wave removes the orphan path entirely.
    """
    deps = get_depends_on("background_render")
    assert "floor_plan_overlay_payload" in deps
    assert "background_prompt" in deps  # 기존 의존성 보존.
    assert "floor_plan_render" in deps
    assert "background_master_plan" in deps


def test_floor_plan_overlay_payload_depends_on_producers():
    """overlay payload 가 floor_plan_prompt + background_master_plan 의 cp 만
    consume — 외부 의존성 추가 X (LLM/image/VLM 호출 0 보장의 일환)."""
    deps = get_depends_on("floor_plan_overlay_payload")
    assert set(deps) == {"floor_plan_prompt", "background_master_plan"}
