"""Block B B5 — `_LEGACY_SCHEMA_BUMP_ALLOWLIST` + `_evaluate_contract_drift()`.

Plan v2.1.3 / spec V5 §4.5 (AC-B4):
- schema_version / config_hash mismatch (cp_mismatch) 는 contract_drift 의
  prerequisite — 이전엔 모두 RERUN_SELF + force-like 자동 재실행 (M3 fix).
  본 commit 후 기본 BLOCK 으로 전환 — 사용자 명시 force / 진단 강제.
- 한정적 allowlist (`{entity_t2i}` 만) 는 schema_version mismatch 에 한해
  RERUN_SELF 허용. config_hash mismatch 는 모든 step BLOCK (allowlist 미적용).

scope:
- entity_t2i 외 모든 step: schema/config mismatch → BLOCK
- entity_t2i: schema_version mismatch → RERUN_SELF (legacy bump 호환)
- entity_t2i + config_hash mismatch → BLOCK (allowlist 미적용)
- 기존 test_resume_auto_recovers_stale_default / test_resume_schema_mismatch_
  auto_recovers 는 새 정책으로 마이그레이션.
"""
from __future__ import annotations

import pytest


# ---------------------------------------------------------------------------
# allowlist constant
# ---------------------------------------------------------------------------


def test_allowlist_contains_only_entity_t2i():
    """V1 patch I3 + V2 답변 #3: scene_detail / 기타 step 은 제외.

    rerun_self 시 downstream (shot_dependency_t2i / scene_image_pipeline 등)
    보존 → 새 결과와 기존 downstream 사이 contract drift 위험. 따라서 한정
    allowlist 만 허용.
    """
    from app.core.step_manifest import _LEGACY_SCHEMA_BUMP_ALLOWLIST

    assert _LEGACY_SCHEMA_BUMP_ALLOWLIST == frozenset({"entity_t2i"})


def test_scene_detail_not_in_allowlist():
    """downstream cascade 위험 — explicit 제외."""
    from app.core.step_manifest import _LEGACY_SCHEMA_BUMP_ALLOWLIST

    assert "scene_detail" not in _LEGACY_SCHEMA_BUMP_ALLOWLIST


def test_allowlist_is_immutable():
    """frozenset — runtime mutation 불가 (silent allowlist 확장 차단)."""
    from app.core.step_manifest import _LEGACY_SCHEMA_BUMP_ALLOWLIST

    with pytest.raises(AttributeError):
        _LEGACY_SCHEMA_BUMP_ALLOWLIST.add("scene_detail")


# ---------------------------------------------------------------------------
# `_evaluate_contract_drift()` helper
# ---------------------------------------------------------------------------


def _make_runner(step_id: str):
    from app.core.step_runner import StepRunner

    runner = StepRunner.__new__(StepRunner)
    runner.step_id = step_id
    return runner


class TestEvaluateContractDriftAllowlist:
    def test_entity_t2i_schema_version_mismatch_returns_rerun_self(self):
        """entity_t2i + schema_version mismatch → RERUN_SELF (legacy bump 허용)."""
        from app.core.step_runner import ResumeAction

        runner = _make_runner("entity_t2i")
        decision = runner._evaluate_contract_drift(
            "schema_version mismatch: 체크포인트=1, 현재=2"
        )

        assert decision.action == ResumeAction.RERUN_SELF
        assert decision.origin == "contract_drift"
        # allowlist 명시
        assert "allowlist" in decision.reason.lower() or "entity_t2i" in decision.reason


class TestEvaluateContractDriftDefault:
    def test_non_allowlisted_schema_version_mismatch_returns_block(self):
        """allowlist 외 step + schema_version mismatch → BLOCK."""
        from app.core.step_runner import ResumeAction

        runner = _make_runner("scene_detail")
        decision = runner._evaluate_contract_drift(
            "schema_version mismatch: 체크포인트=1, 현재=2"
        )

        assert decision.action == ResumeAction.BLOCK
        assert decision.origin == "contract_drift"

    def test_config_hash_mismatch_blocks_even_for_allowlisted_step(self):
        """entity_t2i 라도 config_hash mismatch 는 allowlist 적용 X → BLOCK.

        config_hash 변경은 사용자 의도 (project_config 변경) — 자동 재실행 시
        의도와 다른 결과 생성 위험. allowlist 는 schema_version 한정.
        """
        from app.core.step_runner import ResumeAction

        runner = _make_runner("entity_t2i")
        decision = runner._evaluate_contract_drift(
            "config_hash mismatch: project_config 변경 감지"
        )

        assert decision.action == ResumeAction.BLOCK
        assert decision.origin == "contract_drift"

    def test_unknown_mismatch_pattern_blocks(self):
        """unrecognized mismatch 문자열도 safer default — BLOCK."""
        from app.core.step_runner import ResumeAction

        runner = _make_runner("entity_t2i")
        decision = runner._evaluate_contract_drift("some other mismatch")

        assert decision.action == ResumeAction.BLOCK
        assert decision.origin == "contract_drift"


# ---------------------------------------------------------------------------
# `_evaluate_resume_decision` integration — cp_mismatch path 가 contract_drift
# helper 를 호출하는지 검증 (B11 helper 의 cp_mismatch 분기 wiring).
# ---------------------------------------------------------------------------


class TestResumeDecisionContractDriftIntegration:
    def _make_runner(self, *, step_id: str, cp_mismatch: str, project_config: dict | None = None):
        """_check_cp_mismatch 가 mismatch 반환 → helper 가 _evaluate_contract_drift 호출."""
        from app.core.step_runner import StepRunner

        runner = StepRunner.__new__(StepRunner)
        runner.step_id = step_id
        runner.project_config = project_config or {}
        runner._get_step_run = lambda sid: {
            "status": "completed", "run_id": "r1",
            "started_at": "2026-05-08T00:00:00",
            "completed_count": 1, "applicable_count": 1,
            "recovery_count": 0, "updated_at": "2026-05-08T00:01:00",
        }
        runner.load_checkpoint = lambda: {
            "schema_version": 1, "config_hash": "h", "data": {},
        }
        runner._check_cp_mismatch = lambda c: cp_mismatch
        # verify 는 cp_mismatch 분기에서 호출 안 됨 (호출 시 fail-fast)
        runner._safe_verify_completion = lambda: pytest.fail(
            "verify must not be called when cp_mismatch is present"
        )
        return runner

    def test_cp_schema_mismatch_non_allowlisted_returns_block(self):
        from app.core.step_runner import ResumeAction

        runner = self._make_runner(
            step_id="scene_detail",
            cp_mismatch="schema_version mismatch: 체크포인트=1, 현재=2",
        )
        decision = runner._evaluate_resume_decision(mode="resume")

        assert decision.action == ResumeAction.BLOCK
        assert decision.origin == "contract_drift"

    def test_cp_schema_mismatch_entity_t2i_returns_rerun_self(self):
        """entity_t2i 만 RERUN_SELF — 다른 step 회귀 차단 가드."""
        from app.core.step_runner import ResumeAction

        runner = self._make_runner(
            step_id="entity_t2i",
            cp_mismatch="schema_version mismatch: 체크포인트=1, 현재=2",
        )
        decision = runner._evaluate_resume_decision(mode="resume")

        assert decision.action == ResumeAction.RERUN_SELF
        assert decision.origin == "contract_drift"

    def test_cp_config_hash_mismatch_blocks_for_any_step(self):
        from app.core.step_runner import ResumeAction

        # entity_t2i 라도 config_hash 는 BLOCK
        for step_id in ("entity_t2i", "scene_detail", "background_classify"):
            runner = self._make_runner(
                step_id=step_id,
                cp_mismatch="config_hash mismatch: project_config 변경 감지",
            )
            decision = runner._evaluate_resume_decision(mode="resume")
            assert decision.action == ResumeAction.BLOCK, f"step={step_id} 가 BLOCK 이 아님"
            assert decision.origin == "contract_drift"


class TestStrictResumeLegacyOverridePreserved:
    """기존 strict_resume=True legacy 동작 — contract policy 보다 우선.

    operator 가 strict_resume=True 명시 → 모든 mismatch BLOCK + legacy message
    형식 보존 (test_dispatcher_modes::test_resume_with_strict_flag_raises_legacy
    회귀 차단 가드).
    """

    def test_strict_resume_overrides_contract_allowlist(self):
        """strict_resume=True + entity_t2i + schema mismatch → BLOCK with legacy message
        (origin=strict_resume), NOT RERUN_SELF (allowlist 무시).
        """
        from app.core.step_runner import ResumeAction, StepRunner

        runner = StepRunner.__new__(StepRunner)
        runner.step_id = "entity_t2i"
        runner.project_config = {"strict_resume": True}
        runner._get_step_run = lambda sid: {
            "status": "completed", "run_id": "r1",
            "started_at": None,
            "completed_count": 1, "applicable_count": 1,
            "recovery_count": 0, "updated_at": None,
        }
        runner.load_checkpoint = lambda: {
            "schema_version": 1, "config_hash": "h", "data": {},
        }
        runner._check_cp_mismatch = lambda c: "schema_version mismatch: 1 → 2"
        runner._safe_verify_completion = lambda: pytest.fail(
            "verify must not be called when cp_mismatch is present"
        )

        decision = runner._evaluate_resume_decision(mode="resume")

        assert decision.action == ResumeAction.BLOCK
        assert decision.origin == "strict_resume"
        # legacy message 형식 보존 — raw mismatch
        assert "schema_version mismatch" in decision.reason
