"""B1 patch (Block A closure stabilization) — `_save_checkpoint_data` fail-fast.

silent fallback 정책 (`feedback_no_silent_fallback.md`):
- 옛 동작: target manifest missing/unreadable 시 `logger.warning + return` →
  mutator 가 성공 status 로 종료 (cp 저장은 silently skip). archive 만 보존.
- 새 동작: 두 path 모두 `AppError(t2i_review.checkpoint_save_failed)` raise.
  caller (StepRunner) 가 step 실패로 surface — silent corruption 차단.
"""
from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from app.core.errors import AppError
from app.core.steps.t2i_review_step import T2iReviewStep


def _make_step(tmp_path: Path) -> T2iReviewStep:
    """T2iReviewStep instance — projects_dir/checkpoints path 만 stub.

    `_save_checkpoint_data` 는 self.project_id, self.episode_id, settings.projects_dir
    를 사용 — 그 외 의존성은 호출되지 않음.
    """
    step = T2iReviewStep.__new__(T2iReviewStep)
    step.project_id = "p1"
    step.episode_id = "e1"
    return step


def test_save_checkpoint_data_raises_when_manifest_missing(tmp_path, monkeypatch):
    """B1 patch: target manifest 부재 → AppError fail-fast (silent skip 금지).

    옛 코드는 `logger.warning + return` — mutator 가 성공처럼 끝나지만 cp 미갱신.
    새 코드는 `AppError(t2i_review.checkpoint_save_failed)` raise.
    """
    step = _make_step(tmp_path)
    monkeypatch.setattr(
        "app.core.config.settings",
        MagicMock(projects_dir=str(tmp_path)),
    )
    # 의도적으로 manifest 디렉토리/파일 안 만듦 — manifest.exists() == False.

    with pytest.raises(AppError) as exc_info:
        step._save_checkpoint_data("scene_detail", {"scenes": []})

    assert exc_info.value.code == "t2i_review.checkpoint_save_failed"
    assert "missing" in exc_info.value.message.lower()


def test_save_checkpoint_data_raises_when_manifest_unreadable(tmp_path, monkeypatch):
    """B1 patch: target manifest unreadable (read_json_safe -> None) → AppError fail-fast.

    옛 코드는 archive 만 남기고 silently return — 새 코드는 raise + archive
    pre-mutation state 보존.
    """
    step = _make_step(tmp_path)
    # manifest 만 만들어 exists() True 통과시킴.
    cp_dir = tmp_path / "p1" / "checkpoints" / "episodes" / "e1" / "scene_detail"
    cp_dir.mkdir(parents=True)
    manifest = cp_dir / "manifest.json"
    manifest.write_text("CORRUPT JSON {")  # parse 실패 시뮬

    monkeypatch.setattr(
        "app.core.config.settings",
        MagicMock(projects_dir=str(tmp_path)),
    )

    with pytest.raises(AppError) as exc_info:
        step._save_checkpoint_data("scene_detail", {"scenes": []})

    assert exc_info.value.code == "t2i_review.checkpoint_save_failed"
    # archive 보존 — 추가 검증
    archives = list(cp_dir.glob("manifest_*_pre_review.json"))
    assert len(archives) >= 1, (
        "archive 가 mutation 전 보존되어야 — fail-fast 가 raise 전 archive copy 수행"
    )


def test_save_checkpoint_data_raises_when_archive_copy_fails(tmp_path, monkeypatch):
    """B1 v2 (Codex IMPORTANT #2): shutil.copy2 가 OSError raise 시 (disk full /
    permission 등) 도 AppError(t2i_review.checkpoint_save_failed) 로 surface.

    fail-fast contract symmetry — manifest missing/unreadable 과 archive copy 실패
    모두 같은 AppError code 로 통일. raw OSError leak 시 caller 의 try 가
    (AppError, KeyError, IndexError) 만 catch — silent break path 차단.
    """
    step = _make_step(tmp_path)
    cp_dir = tmp_path / "p1" / "checkpoints" / "episodes" / "e1" / "scene_detail"
    cp_dir.mkdir(parents=True)
    # manifest 는 존재 (exists check 통과)
    manifest = cp_dir / "manifest.json"
    manifest.write_text('{"data": {}}')

    monkeypatch.setattr(
        "app.core.config.settings",
        MagicMock(projects_dir=str(tmp_path)),
    )

    # shutil.copy2 가 OSError raise — disk full / permission denied 시뮬
    def _fake_copy2(*args, **kwargs):
        raise OSError("simulated disk full")

    # shutil 은 _save_checkpoint_data 안에서 lazy import 되므로 global module 의
    # copy2 를 직접 patch (sys.modules['shutil'].copy2).
    import shutil
    monkeypatch.setattr(shutil, "copy2", _fake_copy2)

    with pytest.raises(AppError) as exc_info:
        step._save_checkpoint_data("scene_detail", {"scenes": []})

    assert exc_info.value.code == "t2i_review.checkpoint_save_failed"
    # original OSError chained
    assert isinstance(exc_info.value.__cause__, OSError)
    assert "disk full" in str(exc_info.value.__cause__)
