"""Phase 1.5 — step_type / lifecycle / resume_sensitive 필드 검증.

참조: docs/architecture-refactor-final/01-principles-revised.md §4, §5
"""
import pytest
from app.core.step_manifest import STEP_MANIFEST

_STEP_TYPES = {"transform", "projection", "asset", "editorial"}
_LIFECYCLES = {"active", "deprecated", "removed"}


def test_all_steps_have_step_type():
    missing = [sid for sid, s in STEP_MANIFEST.items() if "step_type" not in s]
    assert not missing, f"step_type missing: {missing}"


def test_editorial_cascade_disabled_by_default():
    """editorial step의 downstream stale cascade는 기본 비활성.

    정책: editorial step(t2i_review 등)은 1 pass만 실행. cascade 활성 시
    매 pass마다 새 fixes 발견 → 무한 수렴 실패. 명시적으로
    `invalidate_downstream_on_edit: True`로 설정한 경우에만 cascade.
    """
    for sid, s in STEP_MANIFEST.items():
        if s.get("step_type") != "editorial":
            continue
        cascade = s.get("invalidate_downstream_on_edit", False)
        # truthy/falsy — None, 0, False 등 falsy 전부 허용 (기본값 False와 동일 의미)
        assert not cascade, (
            f"{sid}: editorial에 invalidate_downstream_on_edit={cascade!r} 설정됨. "
            f"1-pass 정책과 충돌 — 의도적이라면 이 테스트를 수정해 명시하라."
        )


def test_step_catalog_exposes_invalidate_downstream_on_edit():
    """StepCatalog가 invalidate_downstream_on_edit 필드를 노출 (manifest 직접 참조 금지 원칙).

    소비자(UI/API/tooling)가 STEP_CATALOG만 봐도 policy flag 확인 가능해야 함.
    """
    from app.core.step_catalog import STEP_CATALOG
    assert hasattr(next(iter(STEP_CATALOG.values())), "invalidate_downstream_on_edit")
    # 현재는 모든 editorial step이 False (기본값)
    for sid, entry in STEP_CATALOG.items():
        if entry.step_type == "editorial":
            assert entry.invalidate_downstream_on_edit is False, (
                f"{sid}: StepEntry.invalidate_downstream_on_edit={entry.invalidate_downstream_on_edit}"
            )


def test_all_steps_have_lifecycle():
    missing = [sid for sid, s in STEP_MANIFEST.items() if "lifecycle" not in s]
    assert not missing, f"lifecycle missing: {missing}"


def test_step_type_values_are_known():
    invalid = [
        (sid, s["step_type"])
        for sid, s in STEP_MANIFEST.items()
        if s["step_type"] not in _STEP_TYPES
    ]
    assert not invalid, f"unknown step_type: {invalid}"


def test_lifecycle_values_are_known():
    invalid = [
        (sid, s["lifecycle"])
        for sid, s in STEP_MANIFEST.items()
        if s["lifecycle"] not in _LIFECYCLES
    ]
    assert not invalid, f"unknown lifecycle: {invalid}"


def test_image_steps_are_asset():
    """category=image는 모두 step_type=asset."""
    violations = [
        sid for sid, s in STEP_MANIFEST.items()
        if s["category"] == "image" and s["step_type"] != "asset"
    ]
    assert not violations, f"image step not asset: {violations}"


def test_editorial_steps_declare_modifies_checkpoints():
    """editorial step은 modifies_checkpoints 필드 필수 (deprecated 제외)."""
    for sid, s in STEP_MANIFEST.items():
        if s["step_type"] == "editorial" and s["lifecycle"] == "active":
            assert "modifies_checkpoints" in s, f"{sid}: missing modifies_checkpoints"
            assert isinstance(s["modifies_checkpoints"], list)
            assert len(s["modifies_checkpoints"]) > 0, f"{sid}: empty modifies_checkpoints"


def test_editorial_modifies_references_are_valid():
    """editorial이 수정한다고 선언한 step_id는 모두 manifest에 존재."""
    for sid, s in STEP_MANIFEST.items():
        if s["step_type"] == "editorial" and s["lifecycle"] == "active":
            for target in s.get("modifies_checkpoints", []):
                assert target in STEP_MANIFEST, (
                    f"{sid} modifies {target!r} but target not in manifest"
                )


def test_deprecated_steps_declare_replaced_by_or_disabled():
    """deprecated step은 replaced_by 선언 또는 applicability=disabled."""
    for sid, s in STEP_MANIFEST.items():
        if s["lifecycle"] == "deprecated":
            has_replacement = "replaced_by" in s
            is_disabled = s["applicability"] == "disabled"
            assert has_replacement or is_disabled, (
                f"{sid}: deprecated but neither replaced_by nor disabled"
            )


def test_replaced_by_targets_are_valid():
    """replaced_by에 명시된 step_id(s)는 manifest에 존재."""
    for sid, s in STEP_MANIFEST.items():
        replacement = s.get("replaced_by")
        if not replacement:
            continue
        targets = [t.strip() for t in replacement.split(",")]
        for target in targets:
            assert target in STEP_MANIFEST, (
                f"{sid}.replaced_by references unknown step {target!r}"
            )


def test_resume_sensitive_is_bool():
    """resume_sensitive는 bool 또는 미정의 (기본 False)."""
    for sid, s in STEP_MANIFEST.items():
        if "resume_sensitive" in s:
            assert isinstance(s["resume_sensitive"], bool), (
                f"{sid}: resume_sensitive not bool: {type(s['resume_sensitive'])}"
            )


def test_resume_sensitive_steps_are_shot_more_cohort():
    """shot-more 브랜치 신규 3 step만 resume_sensitive=True (현재 규칙)."""
    expected = {"scene_camera_flow", "shot_staging", "scene_consistency"}
    actual = {sid for sid, s in STEP_MANIFEST.items() if s.get("resume_sensitive")}
    assert actual == expected, f"resume_sensitive mismatch: {actual} != {expected}"


def test_active_steps_count_matches_baseline():
    """active lifecycle step 수 = baseline 53 (64 total - 10 deprecated - 1 removed).
    Phase 7 6 step 추가 (background_classify/master_plan/floor_plan_prompt+render/background_prompt+render)
    + Phase 5 4 step deprecated (location_floor_plan/background_planner/background_chain_planning/background_chain_render)
    = net +2 active (2026-04-29).
    reference-necessity Phase 2: episode_reference_policy 추가 (+1 active, 2026-05-23).
    W19/W20 cleanup wave (2026-05-27): W19B-1 floor_plan_overlay_payload +1 / W20A
    base_location_dossier +1 / W20A2 floor_plan_geometry_readback +1 /
    W20B shot_aware_bg_render_plan +1 (모두 default OFF opt-in, manifest active).
    W21B-wave-4 floor_plan_semantic_readback +1 (2026-05-29) + C2
    shot_projection_card +1 (2026-05-30, default OFF opt-in) → 55.
    W21B-w5 bg_space_partition +1 (2026-05-31, default OFF opt-in) → 56.
    W21B-w5 STEP5-B floor_plan_light_sidecar +1 (2026-05-31, default OFF) → 57.
    W21B dwelling_zone_map +1 (2026-06-08, default OFF opt-in) → 58.
    W21B space_set_bg +1 (2026-06-10, default OFF opt-in) → 59.
    W21B-W8 outdoor_site_layout +1 (2026-06-12, default OFF opt-in) → 62 active."""
    active = [sid for sid, s in STEP_MANIFEST.items() if s["lifecycle"] == "active"]
    assert len(active) == 62, f"active count: {len(active)}"


def test_removed_steps_are_disabled():
    """lifecycle=removed는 applicability=disabled여야 함 (자동 실행 경로에서 제외)."""
    for sid, s in STEP_MANIFEST.items():
        if s["lifecycle"] == "removed":
            assert s["applicability"] == "disabled", (
                f"{sid}: lifecycle=removed but applicability={s['applicability']!r}"
            )


def test_step_type_distribution():
    """baseline 분포: transform 53, editorial 2, asset 9 (Phase 7 +4 transform +2 asset, 2026-04-29).

    Phase 7 추가 transform: background_classify, background_master_plan,
    floor_plan_prompt, background_prompt.
    Phase 7 추가 asset: floor_plan_render, background_render.
    reference-necessity Phase 2: episode_reference_policy (+1 transform, 2026-05-23).
    W19/W20 cleanup wave (2026-05-27): W19B-1 floor_plan_overlay_payload +1 /
    W20A base_location_dossier +1 / W20A2 floor_plan_geometry_readback +1 /
    W20B shot_aware_bg_render_plan +1 = +4 transform.
    W21B-wave-4 floor_plan_semantic_readback +1 (2026-05-29) + C2
    shot_projection_card +1 (2026-05-30) → transform 55.
    W21B-w5 bg_space_partition +1 (2026-05-31) → transform 56.
    W21B-w5 STEP5-B floor_plan_light_sidecar +1 (2026-05-31, step_type=asset) → asset 10.
    W21B dwelling_zone_map +1 (2026-06-08, step_type=transform) → transform 57.
    W21B space_set_bg +1 (2026-06-10, step_type=transform) → transform 58.
    W21B fresh-E2E fix (2026-06-10): space_set_bg analysis→image 이동에 따라
    step_type transform→asset (image 불변식) → transform 57 / asset 11.
    W21B-W8 outdoor_site_layout +1 (2026-06-12, step_type=transform) → transform 60."""
    from collections import Counter
    dist = Counter(s["step_type"] for s in STEP_MANIFEST.values())
    assert dist["transform"] == 60
    assert dist["editorial"] == 2
    assert dist["asset"] == 11
    assert dist.get("projection", 0) == 0  # Phase 2 이후 등장


def test_asset_steps_exact_list():
    """asset step은 10개: image 5 + Phase 5 deprecated 2 (background_chain_render, location_floor_plan)
    + Phase 7 active 2 (floor_plan_render, background_render)
    + W21B-w5 STEP5-B 1 (floor_plan_light_sidecar, 2026-05-31). 2026-05-31 갱신.
    + W21B fresh-E2E fix 1 (space_set_bg analysis→image 이동, 2026-06-10) → 11.

    deprecated entry는 manifest에 남아있고 step_type=asset 유지 (lifecycle만 변경)."""
    expected = {
        "world_guide", "ref_image_gen", "composite_image_gen",
        "character_state_variant", "scene_image_pipeline",
        "background_chain_render",  # Phase 5 deprecated
        "location_floor_plan",  # Phase 5 deprecated
        "floor_plan_render",  # Phase 7 (2026-04-29)
        "background_render",  # Phase 7 (2026-04-29)
        "floor_plan_light_sidecar",  # W21B-w5 STEP5-B (2026-05-31)
        "space_set_bg",  # W21B fresh-E2E fix (2026-06-10, analysis→image)
    }
    actual = {sid for sid, s in STEP_MANIFEST.items() if s["step_type"] == "asset"}
    assert actual == expected, f"asset mismatch: {actual} != {expected}"


def test_t2i_review_modifies_entity_t2i_and_scene_detail():
    """t2i_review는 entity_t2i + scene_detail 체크포인트를 덮어쓴다고 선언해야 함.

    Codex Phase 1 리뷰 C2: modifies_checkpoints → downstream invalidation 연결.
    """
    mods = STEP_MANIFEST["t2i_review"].get("modifies_checkpoints") or []
    assert set(mods) == {"entity_t2i", "scene_detail"}
