"""부분 재생성 회귀 가드 (Task 25, PR 2).

이미지 step의 `cleanup_artifacts` override 부재를 회귀로 보장한다.

배경 (사용자 CAVEAT):
  "이미지는 거의 대부분 재생성 가능해야 해 — cleanup_artifacts override는
   image step에서 절대 금지" (entity/shot/group 부분 재생성 보존).

audit A2 결과: 현재 image_steps.py에는 cleanup_artifacts override **0건** 확인.
  - RefImageGenStep
  - CompositeImageGenStep
  - CharacterStateVariantStep
  - SceneImagePipelineStep
  - BackgroundChainRenderStep (PR 2 Task 23)
  - FloorPlanRenderStep (PR 2 Task 24, soft import)

audit A4 결과: spec §6 "cleanup 예외 시 raise + step_run='failed'" 정책은
override가 0이라 image step에서 trigger 되지 않음 (이론적 충돌만 존재).

이 테스트의 목적: **회귀 가드** — 미래에 누군가 image step에 destructive
cleanup_artifacts override를 추가하면 즉시 catch.

회귀 가드 패턴:
  - `Step.__dict__.get("cleanup_artifacts")` 가 None 이어야 함 (own override 없음)
  - `Step.cleanup_artifacts is StepRunner.cleanup_artifacts` (descriptor identity)
  - `Step(...).cleanup_artifacts()` 호출 시 deleted_db_rows=0 / deleted_files=0 / targets=[]

부분 재생성 endpoint 회귀 시나리오:
  /api/v1/projects/.../images/{image_id}/regenerate
  /api/v1/projects/.../entities/{entity_id}/regenerate-composites
  /api/v1/projects/.../episodes/.../regenerate-needs-fix
는 `tests/test_images_api.py` 및 `tests/services/test_*_service.py` 에서 이미
회귀 가드가 충분히 존재하므로 본 파일은 step-level cleanup 정책만 다룬다.
"""
from __future__ import annotations

import pytest


def _resolve_image_step_classes():
    """image step 클래스 6종 import (Task 24 미완료 시 FloorPlan은 None).

    Task 23 BackgroundChainRenderStep은 main에 이미 wired (commit 시점).
    Task 24 FloorPlanRenderStep은 implementer subagent와 동시 작업 — import
    시도, 실패 시 None (skip). 본 가드는 Task 24가 머지된 직후 즉시 활성.
    """
    from app.core.steps.image_steps import (
        CharacterStateVariantStep,
        CompositeImageGenStep,
        RefImageGenStep,
        SceneImagePipelineStep,
    )

    classes: list[tuple[str, type]] = [
        ("RefImageGenStep", RefImageGenStep),
        ("CompositeImageGenStep", CompositeImageGenStep),
        ("CharacterStateVariantStep", CharacterStateVariantStep),
        ("SceneImagePipelineStep", SceneImagePipelineStep),
    ]

    # Task 23: BackgroundChainRenderStep (main에 이미 존재).
    try:
        from app.core.steps.background_chain_render_step import (
            BackgroundChainRenderStep,
        )
        classes.append(("BackgroundChainRenderStep", BackgroundChainRenderStep))
    except ImportError:
        pass

    # Task 24: FloorPlanRenderStep (parallel implementer).
    try:
        from app.core.steps.floor_plan_render_step import FloorPlanRenderStep
        classes.append(("FloorPlanRenderStep", FloorPlanRenderStep))
    except ImportError:
        pass

    return classes


@pytest.mark.parametrize(
    "step_name,step_cls",
    _resolve_image_step_classes(),
    ids=lambda x: x if isinstance(x, str) else "",
)
def test_image_step_no_cleanup_override(step_name, step_cls):
    """이미지 step은 cleanup_artifacts를 override 하지 않는다 (사용자 CAVEAT).

    검증 순서:
      1. step_cls.__dict__["cleanup_artifacts"] 가 없어야 함 (own attr 부재).
      2. mro 어디에도 base StepRunner 외의 override 가 없어야 함 (중간 mixin/
         서브클래스 override도 금지). identity 비교로 base 와 동일함을 보장.

    회귀 트리거: 누군가 이 step에 cleanup_artifacts 를 추가하면 즉시 fail.
    필요 시 사용자/리뷰 단계에서 명시적으로 본 테스트의 expected list 를
    재논의 (silent merge 차단).
    """
    from app.core.step_runner import StepRunner

    # 1. own attribute 가 없는지 (가장 강한 회귀 가드).
    own = step_cls.__dict__.get("cleanup_artifacts")
    assert own is None, (
        f"{step_name} cleanup_artifacts 직접 override — 부분 재생성 회귀 위험. "
        f"사용자 CAVEAT(image step destructive 금지) 위반. "
        f"의도된 변경이면 본 테스트의 expected list 명시적 수정 필요."
    )

    # 2. mro 전체 — descriptor identity 가 base 와 동일해야 함.
    # (mixin 또는 다른 ancestor 가 override 했어도 잡힘)
    assert step_cls.cleanup_artifacts is StepRunner.cleanup_artifacts, (
        f"{step_name} mro 어딘가에서 cleanup_artifacts 를 shadow 함. "
        f"base StepRunner.cleanup_artifacts identity 와 일치해야 함."
    )


def test_image_step_cleanup_returns_noop_report():
    """6 image step 모두 cleanup_artifacts() 호출 시 noop CleanupReport 반환.

    base StepRunner.cleanup_artifacts 는 deleted_db_rows=0 / deleted_files=0 /
    targets=[] / skipped=[] 을 반환 (frozen dataclass). image step은 모두
    이 base 를 그대로 사용하므로 instance 호출 결과가 모두 noop.

    instance 생성을 피하기 위해 unbound method를 직접 호출:
      `StepRunner.cleanup_artifacts(self_dummy)` — base 가 self 를 거의 안 씀
      (단, override 가능성 회귀 차단 위해 step_cls.cleanup_artifacts 를 unbound
      로 호출해 base 와 동일 동작 보장).
    """
    from app.core.integrity_report import CleanupReport
    from app.core.step_runner import StepRunner

    classes = _resolve_image_step_classes()

    # base 가 self 를 사용하지 않는 noop 임을 가정. instance 없이 unbound 호출.
    # instance 생성은 DB/FK 시드를 요구하므로 회귀 가드의 단순성을 위해 회피.
    sentinel = object()
    base_report = StepRunner.cleanup_artifacts(sentinel)  # type: ignore[arg-type]
    assert isinstance(base_report, CleanupReport)
    assert base_report.deleted_db_rows == 0
    assert base_report.deleted_files == 0
    assert base_report.targets == []
    assert base_report.skipped == []

    # image step 각각이 base 와 identity 동일하므로 동일 결과.
    for step_name, step_cls in classes:
        report = step_cls.cleanup_artifacts(sentinel)  # type: ignore[arg-type]
        assert isinstance(report, CleanupReport), (
            f"{step_name} cleanup_artifacts 가 CleanupReport 가 아닌 다른 타입 반환"
        )
        assert report.deleted_db_rows == 0, (
            f"{step_name} default cleanup 이 row 를 삭제 — destructive!"
        )
        assert report.deleted_files == 0, (
            f"{step_name} default cleanup 이 파일을 삭제 — destructive!"
        )
        assert report.targets == [], f"{step_name} cleanup targets 비어있어야 함"
        assert report.skipped == [], f"{step_name} cleanup skipped 비어있어야 함"


def test_image_step_resolved_minimum_count():
    """방어적 가드 — image step 4개(min)는 항상 import 되어야 한다.

    Task 23/24 가 모두 머지되면 6개. 머지 전에는 최소 4개(image_steps.py
    에서). 이 보다 적으면 someone deleted a step → 회귀 fail.
    """
    classes = _resolve_image_step_classes()
    names = {n for n, _ in classes}
    expected_min = {
        "RefImageGenStep",
        "CompositeImageGenStep",
        "CharacterStateVariantStep",
        "SceneImagePipelineStep",
    }
    missing = expected_min - names
    assert not missing, (
        f"image_steps.py 에서 누락된 step 클래스: {missing}. "
        f"파이프라인 wiring 또는 import 경로 회귀 가능성."
    )
