#!/usr/bin/env python3
"""Tests for output-review.py and output_review_helpers.py — TDD RED phase.

헬퍼 모듈(output_review_helpers)과 메인 스크립트(output-review)에 대한 테스트.
아직 구현이 없으므로 ImportError 또는 AttributeError로 실패하는 것이 정상(RED 단계).
"""

import importlib.util
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# 모듈 임포트 (하이픈 파일명 처리)
# ---------------------------------------------------------------------------

_SCRIPTS_DIR = Path(__file__).parent.parent

# output_review_helpers.py 임포트 (일반 모듈명)
_HELPERS_PATH = _SCRIPTS_DIR / "output_review_helpers.py"
_helpers_spec = importlib.util.spec_from_file_location("output_review_helpers", _HELPERS_PATH)
assert _helpers_spec is not None and _helpers_spec.loader is not None
orh = importlib.util.module_from_spec(_helpers_spec)
sys.modules["output_review_helpers"] = orh
_helpers_spec.loader.exec_module(orh)  # type: ignore[union-attr]

# output-review.py 임포트 (하이픈 파일명)
_MAIN_PATH = _SCRIPTS_DIR / "output-review.py"
_main_spec = importlib.util.spec_from_file_location("output_review", _MAIN_PATH)
assert _main_spec is not None and _main_spec.loader is not None
orm = importlib.util.module_from_spec(_main_spec)
sys.modules["output_review"] = orm
_main_spec.loader.exec_module(orm)  # type: ignore[union-attr]


# ---------------------------------------------------------------------------
# 공통 헬퍼
# ---------------------------------------------------------------------------


def _make_tmp_dirs(tmp_path: Path) -> dict[str, Path]:
    """테스트용 디렉토리 구조 생성."""
    champions_dir = tmp_path / "memory" / "skill-learning" / "champions"
    champions_dir.mkdir(parents=True)
    archive_dir = tmp_path / "memory" / "skill-learning" / "champions-archive"
    archive_dir.mkdir(parents=True)
    learnings_path = tmp_path / "memory" / "skill-learning" / "learnings.jsonl"
    learnings_path.touch()
    skills_shared = tmp_path / "skills" / "shared"
    skills_shared.mkdir(parents=True)
    return {
        "champions_dir": champions_dir,
        "archive_dir": archive_dir,
        "learnings_path": learnings_path,
        "skills_shared": skills_shared,
    }


def _make_eval_axes_json(skills_shared: Path, data: dict[str, list[str]]) -> Path:
    """eval-axes.json 파일 생성."""
    path = skills_shared / "eval-axes.json"
    path.write_text(json.dumps(data), encoding="utf-8")
    return path


# ---------------------------------------------------------------------------
# 1. build_champion_data — 스키마 검증
# ---------------------------------------------------------------------------


class TestBuildChampionData:
    def test_build_champion_data_required_fields(self) -> None:
        """champion 데이터에 필수 필드가 모두 존재해야 한다."""
        data = orh.build_champion_data(
            skill_name="satori-cardnews",
            skill_type="business",
            champion_output="sample output text",
            eval_axes_used=["훅 강도", "시각 밸런스"],
        )
        required_fields = [
            "skill_name",
            "skill_type",
            "champion_output",
            "eval_axes_used",
            "created_at",
            "last_used",
            "status",
            "consecutive_defenses",
            "consecutive_losses",
            "reinit_count_this_month",
            "month_reset_date",
            "init_method",
            "benchmark_source",
        ]
        for field in required_fields:
            assert field in data, f"필드 '{field}' 누락"

    def test_build_champion_data_field_types(self) -> None:
        """각 필드의 타입이 스키마와 일치해야 한다."""
        data = orh.build_champion_data(
            skill_name="satori-cardnews",
            skill_type="business",
            champion_output="output",
            eval_axes_used=["훅 강도"],
        )
        assert isinstance(data["skill_name"], str)
        assert isinstance(data["skill_type"], str)
        assert isinstance(data["champion_output"], str)
        assert isinstance(data["eval_axes_used"], list)
        assert isinstance(data["created_at"], str)
        assert isinstance(data["last_used"], str)
        assert isinstance(data["status"], str)
        assert isinstance(data["consecutive_defenses"], int)
        assert isinstance(data["consecutive_losses"], int)
        assert isinstance(data["reinit_count_this_month"], int)
        assert isinstance(data["month_reset_date"], str)
        assert isinstance(data["init_method"], str)
        assert isinstance(data["benchmark_source"], str)

    def test_build_champion_data_default_values(self) -> None:
        """초기값이 스키마 기본값과 일치해야 한다."""
        data = orh.build_champion_data(
            skill_name="test-skill",
            skill_type="creative",
            champion_output="output",
            eval_axes_used=[],
        )
        assert data["status"] == "active"
        assert data["consecutive_defenses"] == 0
        assert data["consecutive_losses"] == 0
        assert data["reinit_count_this_month"] == 0
        assert data["init_method"] == "full_benchmark"
        assert data["benchmark_source"] == "online_expert"

    def test_build_champion_data_custom_init_method(self) -> None:
        """init_method 및 benchmark_source 커스텀 값이 적용되어야 한다."""
        data = orh.build_champion_data(
            skill_name="test-skill",
            skill_type="analytical",
            champion_output="output",
            eval_axes_used=["accuracy"],
            init_method="manual",
            benchmark_source="internal_team",
        )
        assert data["init_method"] == "manual"
        assert data["benchmark_source"] == "internal_team"

    def test_build_champion_data_skill_name_preserved(self) -> None:
        """skill_name이 정확히 보존되어야 한다."""
        data = orh.build_champion_data(
            skill_name="my-special-skill",
            skill_type="business",
            champion_output="output",
            eval_axes_used=[],
        )
        assert data["skill_name"] == "my-special-skill"

    def test_build_champion_data_eval_axes_preserved(self) -> None:
        """eval_axes_used 리스트가 그대로 저장되어야 한다."""
        axes = ["훅 강도", "시각 밸런스", "정보 밀도"]
        data = orh.build_champion_data(
            skill_name="test-skill",
            skill_type="business",
            champion_output="output",
            eval_axes_used=axes,
        )
        assert data["eval_axes_used"] == axes


# ---------------------------------------------------------------------------
# 2. save_champion / load_champion
# ---------------------------------------------------------------------------


class TestSaveAndLoadChampion:
    def test_save_and_load_champion_roundtrip(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """저장 후 로드하면 동일한 데이터가 반환되어야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        champion_data = orh.build_champion_data(
            skill_name="satori-cardnews",
            skill_type="business",
            champion_output="test output",
            eval_axes_used=["훅 강도"],
        )
        orh.save_champion("satori-cardnews", champion_data)
        loaded = orh.load_champion("satori-cardnews")

        assert loaded is not None
        assert loaded["skill_name"] == champion_data["skill_name"]
        assert loaded["champion_output"] == champion_data["champion_output"]
        assert loaded["eval_axes_used"] == champion_data["eval_axes_used"]

    def test_save_champion_returns_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """save_champion은 저장된 파일 경로를 반환해야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        data = orh.build_champion_data(
            skill_name="test-skill",
            skill_type="business",
            champion_output="output",
            eval_axes_used=[],
        )
        result_path = orh.save_champion("test-skill", data)

        assert isinstance(result_path, Path)
        assert result_path.exists()
        assert result_path.name == "test-skill.json"

    def test_save_champion_creates_valid_json(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """저장된 파일이 유효한 JSON이어야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        data = orh.build_champion_data(
            skill_name="json-test",
            skill_type="business",
            champion_output="output",
            eval_axes_used=["axis1"],
        )
        saved_path = orh.save_champion("json-test", data)
        raw = saved_path.read_text(encoding="utf-8")
        parsed = json.loads(raw)
        assert parsed["skill_name"] == "json-test"


# ---------------------------------------------------------------------------
# 3. load_champion — 존재하지 않는 스킬
# ---------------------------------------------------------------------------


class TestLoadChampionNotExists:
    def test_load_champion_returns_none_when_missing(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """존재하지 않는 스킬 로드 시 None을 반환해야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        result = orh.load_champion("nonexistent-skill")
        assert result is None

    def test_load_champion_does_not_raise(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """존재하지 않는 스킬 로드 시 예외가 발생하지 않아야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        # Should not raise
        result = orh.load_champion("missing-skill-xyz")
        assert result is None


# ---------------------------------------------------------------------------
# 4. archive_champion
# ---------------------------------------------------------------------------


class TestArchiveChampion:
    def test_archive_champion_moves_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """기존 챔피언을 아카이브하면 타임스탬프 파일명으로 이동해야 한다."""
        dirs = _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        # 챔피언 파일 직접 생성
        champion_data = orh.build_champion_data(
            skill_name="archive-test",
            skill_type="business",
            champion_output="old output",
            eval_axes_used=["axis1"],
        )
        orh.save_champion("archive-test", champion_data)

        archived_path = orh.archive_champion("archive-test")

        assert archived_path is not None
        assert archived_path.exists()
        # 아카이브 디렉토리 하위에 있어야 한다
        assert dirs["archive_dir"] in archived_path.parents or str(dirs["archive_dir"]) in str(archived_path)
        # 원본 파일은 삭제되었거나 아카이브로 이동됨
        assert archived_path.suffix == ".json"

    def test_archive_champion_filename_has_timestamp(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """아카이브 파일명에 타임스탬프가 포함되어야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        data = orh.build_champion_data(
            skill_name="ts-test",
            skill_type="business",
            champion_output="output",
            eval_axes_used=[],
        )
        orh.save_champion("ts-test", data)
        archived_path = orh.archive_champion("ts-test")

        assert archived_path is not None
        # 파일명에 숫자(타임스탬프)가 포함되어야 함
        stem = archived_path.stem
        assert any(ch.isdigit() for ch in stem), f"타임스탬프 없음: {stem}"

    def test_archive_champion_original_removed(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """아카이브 후 원본 champions/{skill}.json이 존재하지 않아야 한다."""
        dirs = _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        data = orh.build_champion_data(
            skill_name="remove-test",
            skill_type="business",
            champion_output="output",
            eval_axes_used=[],
        )
        orh.save_champion("remove-test", data)
        original = dirs["champions_dir"] / "remove-test.json"
        assert original.exists()

        orh.archive_champion("remove-test")
        assert not original.exists()


# ---------------------------------------------------------------------------
# 5. archive_champion — 존재하지 않는 챔피언
# ---------------------------------------------------------------------------


class TestArchiveChampionNoExisting:
    def test_archive_nonexistent_champion_returns_none(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """아카이브할 챔피언이 없으면 None을 반환해야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        result = orh.archive_champion("does-not-exist")
        assert result is None

    def test_archive_nonexistent_does_not_raise(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """아카이브할 챔피언이 없어도 예외가 발생하지 않아야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        # Should not raise
        result = orh.archive_champion("ghost-skill")
        assert result is None


# ---------------------------------------------------------------------------
# 6. append_learning
# ---------------------------------------------------------------------------


class TestAppendLearning:
    def test_append_learning_increases_line_count(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """append_learning 호출 시 learnings.jsonl 줄 수가 1 증가해야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        learnings_path = tmp_path / "memory" / "skill-learning" / "learnings.jsonl"
        initial_lines = len(learnings_path.read_text(encoding="utf-8").splitlines())

        entry = {
            "skill_name": "test-skill",
            "winner": "A",
            "reason": "더 나은 훅",
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }
        orh.append_learning(entry)

        new_lines = len(learnings_path.read_text(encoding="utf-8").splitlines())
        assert new_lines == initial_lines + 1

    def test_append_learning_valid_json_line(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """append된 줄이 유효한 JSON이어야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        entry = {"skill_name": "json-skill", "event": "defense", "score": 0.85}
        orh.append_learning(entry)

        learnings_path = tmp_path / "memory" / "skill-learning" / "learnings.jsonl"
        last_line = learnings_path.read_text(encoding="utf-8").strip().splitlines()[-1]
        parsed = json.loads(last_line)
        assert parsed["skill_name"] == "json-skill"

    def test_append_learning_multiple_entries(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """여러 번 append 시 각각 줄로 저장되어야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        for i in range(3):
            orh.append_learning({"index": i, "skill_name": f"skill-{i}"})

        learnings_path = tmp_path / "memory" / "skill-learning" / "learnings.jsonl"
        lines = [l for l in learnings_path.read_text(encoding="utf-8").splitlines() if l.strip()]
        assert len(lines) == 3

    def test_append_learning_does_not_overwrite(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """append_learning은 기존 내용을 덮어쓰지 않아야 한다 (append-only)."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        learnings_path = tmp_path / "memory" / "skill-learning" / "learnings.jsonl"
        learnings_path.write_text(json.dumps({"existing": "entry"}) + "\n", encoding="utf-8")

        orh.append_learning({"new": "entry"})

        lines = [l for l in learnings_path.read_text(encoding="utf-8").splitlines() if l.strip()]
        assert len(lines) == 2
        assert json.loads(lines[0])["existing"] == "entry"


# ---------------------------------------------------------------------------
# 7. load_eval_axes
# ---------------------------------------------------------------------------


class TestLoadEvalAxes:
    def test_load_eval_axes_returns_list(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """eval-axes.json에서 해당 스킬의 평가 축 리스트를 반환해야 한다."""
        dirs = _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        _make_eval_axes_json(
            dirs["skills_shared"],
            {
                "satori-cardnews": ["훅 강도", "시각 밸런스", "정보 밀도"],
                "other-skill": ["accuracy"],
            },
        )

        axes = orh.load_eval_axes("satori-cardnews")
        assert isinstance(axes, list)
        assert axes == ["훅 강도", "시각 밸런스", "정보 밀도"]

    def test_load_eval_axes_correct_skill(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """다른 스킬의 축과 혼동되지 않아야 한다."""
        dirs = _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        _make_eval_axes_json(
            dirs["skills_shared"],
            {
                "skill-a": ["axis-a1", "axis-a2"],
                "skill-b": ["axis-b1"],
            },
        )

        axes_a = orh.load_eval_axes("skill-a")
        axes_b = orh.load_eval_axes("skill-b")

        assert axes_a == ["axis-a1", "axis-a2"]
        assert axes_b == ["axis-b1"]


# ---------------------------------------------------------------------------
# 8. load_eval_axes — 없는 스킬
# ---------------------------------------------------------------------------


class TestLoadEvalAxesMissingSkill:
    def test_missing_skill_returns_empty_list(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """eval-axes.json에 없는 스킬은 빈 리스트를 반환해야 한다."""
        dirs = _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        _make_eval_axes_json(
            dirs["skills_shared"],
            {"existing-skill": ["axis1"]},
        )

        result = orh.load_eval_axes("nonexistent-skill")
        assert result == []

    def test_missing_eval_axes_file_returns_empty_list(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """eval-axes.json 파일 자체가 없어도 빈 리스트를 반환해야 한다."""
        _make_tmp_dirs(tmp_path)
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))

        # eval-axes.json을 생성하지 않음
        result = orh.load_eval_axes("any-skill")
        assert result == []


# ---------------------------------------------------------------------------
# 9. record_defense
# ---------------------------------------------------------------------------


class TestRecordDefense:
    def test_record_defense_increments_consecutive_defenses(self) -> None:
        """방어 기록 시 consecutive_defenses가 1 증가해야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 2,
            "consecutive_losses": 1,
            "status": "active",
        }
        updated = orh.record_defense(champion)
        assert updated["consecutive_defenses"] == 3

    def test_record_defense_resets_consecutive_losses(self) -> None:
        """방어 기록 시 consecutive_losses가 0으로 리셋되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 2,
            "status": "active",
        }
        updated = orh.record_defense(champion)
        assert updated["consecutive_losses"] == 0

    def test_record_defense_does_not_modify_original(self) -> None:
        """record_defense는 원본 dict를 변경하거나 변경된 복사본을 반환해도 무방하지만
        반환값이 올바른 상태여야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 0,
            "status": "active",
        }
        updated = orh.record_defense(champion)
        assert updated["consecutive_defenses"] == 1
        assert updated["consecutive_losses"] == 0


# ---------------------------------------------------------------------------
# 10. record_loss
# ---------------------------------------------------------------------------


class TestRecordLoss:
    def test_record_loss_increments_consecutive_losses(self) -> None:
        """패배 기록 시 consecutive_losses가 1 증가해야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 3,
            "consecutive_losses": 0,
            "status": "active",
        }
        updated = orh.record_loss(champion)
        assert updated["consecutive_losses"] == 1

    def test_record_loss_resets_consecutive_defenses(self) -> None:
        """패배 기록 시 consecutive_defenses가 0으로 리셋되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 5,
            "consecutive_losses": 0,
            "status": "active",
        }
        updated = orh.record_loss(champion)
        assert updated["consecutive_defenses"] == 0

    def test_record_loss_from_zero(self) -> None:
        """0에서 패배 기록 시 consecutive_losses가 1이 되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 0,
            "status": "active",
        }
        updated = orh.record_loss(champion)
        assert updated["consecutive_losses"] == 1
        assert updated["consecutive_defenses"] == 0


# ---------------------------------------------------------------------------
# 11. update_champion_status — stable
# ---------------------------------------------------------------------------


class TestStatusStable:
    def test_five_consecutive_defenses_becomes_stable(self) -> None:
        """5연속 방어 시 status가 'stable'이 되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 5,
            "consecutive_losses": 0,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "stable"

    def test_more_than_five_defenses_is_stable(self) -> None:
        """5 초과 연속 방어도 'stable'이어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 10,
            "consecutive_losses": 0,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "stable"

    def test_four_consecutive_defenses_not_stable(self) -> None:
        """4연속 방어는 'stable'이 아니어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 4,
            "consecutive_losses": 0,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] != "stable"


# ---------------------------------------------------------------------------
# 12. update_champion_status — unstable
# ---------------------------------------------------------------------------


class TestStatusUnstable:
    def test_three_consecutive_losses_becomes_unstable(self) -> None:
        """3연속 패배 시 status가 'unstable'이 되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 3,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "unstable"

    def test_more_than_three_losses_is_unstable(self) -> None:
        """3 초과 연속 패배도 'unstable'이어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 5,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "unstable"

    def test_two_consecutive_losses_not_unstable(self) -> None:
        """2연속 패배는 'unstable'이 아니어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 2,
            "reinit_count_this_month": 0,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] != "unstable"


# ---------------------------------------------------------------------------
# 13. update_champion_status — manual_intervention_required
# ---------------------------------------------------------------------------


class TestStatusManualIntervention:
    def test_reinit_count_two_becomes_manual_intervention(self) -> None:
        """reinit_count_this_month >= 2 시 'manual_intervention_required'가 되어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 0,
            "reinit_count_this_month": 2,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "manual_intervention_required"

    def test_reinit_count_three_becomes_manual_intervention(self) -> None:
        """reinit_count_this_month >= 3도 'manual_intervention_required'여야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 0,
            "reinit_count_this_month": 3,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] == "manual_intervention_required"

    def test_reinit_count_one_not_manual_intervention(self) -> None:
        """reinit_count_this_month == 1은 'manual_intervention_required'가 아니어야 한다."""
        champion: dict[str, Any] = {
            "consecutive_defenses": 0,
            "consecutive_losses": 0,
            "reinit_count_this_month": 1,
            "last_used": datetime.now(timezone.utc).isoformat(),
            "status": "active",
        }
        updated = orh.update_champion_status(champion)
        assert updated["status"] != "manual_intervention_required"


# ---------------------------------------------------------------------------
# 14. compare_outputs — graceful degradation (빈 eval_axes)
# ---------------------------------------------------------------------------


class TestGracefulDegradation:
    def test_compare_outputs_empty_axes_does_not_raise(self) -> None:
        """eval_axes가 빈 리스트일 때도 compare_outputs가 정상 동작해야 한다."""
        result = orh.compare_outputs(
            output_a="첫 번째 아웃풋입니다.",
            output_b="두 번째 아웃풋입니다.",
            eval_axes=[],
            skill_name="test-skill",
        )
        assert result is not None

    def test_compare_outputs_empty_axes_returns_dict(self) -> None:
        """eval_axes 빈 리스트 시에도 dict를 반환해야 한다."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=[],
            skill_name="test-skill",
        )
        assert isinstance(result, dict)

    def test_compare_outputs_empty_string_outputs(self) -> None:
        """빈 문자열 아웃풋에도 예외 없이 동작해야 한다."""
        result = orh.compare_outputs(
            output_a="",
            output_b="",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        assert result is not None


# ---------------------------------------------------------------------------
# 15. compare_outputs — 결과 구조 검증
# ---------------------------------------------------------------------------


class TestCompareOutputsStructure:
    def test_compare_outputs_has_winner_key(self) -> None:
        """비교 결과에 'winner' 키가 있어야 한다."""
        result = orh.compare_outputs(
            output_a="output A content",
            output_b="output B content",
            eval_axes=["훅 강도", "시각 밸런스"],
            skill_name="satori-cardnews",
        )
        assert "winner" in result

    def test_compare_outputs_has_reason_key(self) -> None:
        """비교 결과에 'reason' 키가 있어야 한다."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        assert "reason" in result

    def test_compare_outputs_has_scores_key(self) -> None:
        """비교 결과에 'scores' 키가 있어야 한다."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        assert "scores" in result

    def test_compare_outputs_winner_is_a_or_b(self) -> None:
        """winner 값은 'A' 또는 'B'여야 한다."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        assert result["winner"] in ("A", "B")

    def test_compare_outputs_scores_is_dict(self) -> None:
        """scores는 dict여야 한다."""
        result = orh.compare_outputs(
            output_a="output A content",
            output_b="output B content",
            eval_axes=["훅 강도", "시각 밸런스"],
            skill_name="satori-cardnews",
        )
        assert isinstance(result["scores"], dict)

    def test_compare_outputs_scores_values_are_valid(self) -> None:
        """scores 내 값이 유효한 형식이어야 한다 (list 또는 numeric, 또는 빈 dict)."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        scores = result["scores"]
        assert isinstance(scores, dict)
        for key, val in scores.items():
            assert isinstance(val, (list, int, float)), f"scores[{key}]는 list 또는 숫자여야 함"

    def test_compare_outputs_reason_is_string(self) -> None:
        """reason 값이 문자열이어야 한다."""
        result = orh.compare_outputs(
            output_a="output A",
            output_b="output B",
            eval_axes=["axis1"],
            skill_name="test-skill",
        )
        assert isinstance(result["reason"], str)


# ---------------------------------------------------------------------------
# 추가: get_workspace_root
# ---------------------------------------------------------------------------


class TestGetWorkspaceRoot:
    def test_get_workspace_root_from_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """WORKSPACE_ROOT 환경변수가 설정되어 있으면 해당 경로를 반환해야 한다."""
        monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
        root = orh.get_workspace_root()
        assert isinstance(root, Path)
        assert root == tmp_path

    def test_get_workspace_root_returns_path_type(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """반환값이 Path 타입이어야 한다."""
        monkeypatch.delenv("WORKSPACE_ROOT", raising=False)
        root = orh.get_workspace_root()
        assert isinstance(root, Path)


# ---------------------------------------------------------------------------
# Phase 2 공통 헬퍼
# ---------------------------------------------------------------------------

_AI_MODULE_PATH = _SCRIPTS_DIR / "output_review_ai.py"


def _mock_anthropic_response(content_text: str) -> MagicMock:
    """Create a mock Anthropic API response."""
    mock_content = MagicMock()
    mock_content.text = content_text
    mock_response = MagicMock()
    mock_response.content = [mock_content]
    return mock_response


# AI 비교 결과 mock 예시 (position-based)
MOCK_COMPARISON_RESULT: dict[str, Any] = {
    "winner": "1",
    "reason": "아웃풋 1이 훅 강도와 정보 밀도에서 우수함",
    "scores": {
        "훅 강도": [4, 3],
        "정보 밀도": [5, 3],
    },
}

# winner가 A/B로 매핑된 AI 비교 결과
MOCK_COMPARISON_RESULT_AB: dict[str, Any] = {
    "winner": "A",
    "reason": "Output A가 훅 강도와 정보 밀도에서 우수함",
    "scores": {
        "훅 강도": [4, 3],
        "정보 밀도": [5, 3],
    },
}

MOCK_DELTA_RESULT_IMPROVED: dict[str, Any] = {
    "improved": True,
    "comparison": {"훅 강도": [3, 5], "정보 밀도": [3, 4]},
    "reason": "v2가 훅 강도와 정보 밀도에서 v1보다 높은 점수를 기록함",
}

MOCK_DELTA_RESULT_NOT_IMPROVED: dict[str, Any] = {
    "improved": False,
    "comparison": {"훅 강도": [5, 3], "정보 밀도": [4, 3]},
    "reason": "v2가 v1보다 낮은 점수를 기록하여 개선 실패",
}

MOCK_CROSS_MODEL_PASS: dict[str, Any] = {
    "verdict": "pass",
    "suggestions": [],
}

MOCK_CROSS_MODEL_IMPROVE: dict[str, Any] = {
    "verdict": "improve",
    "suggestions": ["훅을 더 강하게", "정보를 더 구체적으로"],
}

MOCK_INIT_ENHANCEMENT_RESULT: dict[str, Any] = {
    "champion_output": "개선된 최종 챔피언 아웃풋",
    "init_process": {
        "ab_comparison": MOCK_COMPARISON_RESULT_AB,
        "benchmark_result": {"expert_text": "전문가 아웃풋 예시"},
        "improvement_applied": True,
        "delta_result": MOCK_DELTA_RESULT_IMPROVED,
    },
    "learnings": ["훅 강도 개선이 효과적", "정보 밀도 상향이 품질 향상에 기여"],
}


# ---------------------------------------------------------------------------
# 16. compare_outputs AI 호출 모킹 테스트 (TestCompareOutputsAI)
# ---------------------------------------------------------------------------


class TestCompareOutputsAI:
    """compare_outputs_ai 함수의 구조 및 동작을 mock으로 검증한다."""

    def test_compare_outputs_ai_returns_correct_structure(self) -> None:
        """compare_outputs_ai가 winner, reason, scores 키를 포함한 dict를 반환해야 한다."""
        with patch.dict("sys.modules", {"output_review_ai": MagicMock()}):
            import output_review_ai as orai  # type: ignore[import]

            orai.compare_outputs_ai.return_value = MOCK_COMPARISON_RESULT_AB

            result = orai.compare_outputs_ai(
                output_a="첫 번째 아웃풋",
                output_b="두 번째 아웃풋",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="satori-cardnews",
            )

            assert "winner" in result
            assert "reason" in result
            assert "scores" in result

    def test_compare_outputs_ai_winner_is_a_or_b(self) -> None:
        """winner 값은 'A' 또는 'B'여야 한다."""
        with patch.dict("sys.modules", {"output_review_ai": MagicMock()}):
            import output_review_ai as orai  # type: ignore[import]

            orai.compare_outputs_ai.return_value = MOCK_COMPARISON_RESULT_AB

            result = orai.compare_outputs_ai(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
            )

            assert result["winner"] in ("A", "B")

    def test_compare_outputs_ai_scores_structure_per_axis(self) -> None:
        """scores는 평가 축별 [a_score, b_score] 리스트 구조여야 한다."""
        with patch.dict("sys.modules", {"output_review_ai": MagicMock()}):
            import output_review_ai as orai  # type: ignore[import]

            orai.compare_outputs_ai.return_value = MOCK_COMPARISON_RESULT_AB

            result = orai.compare_outputs_ai(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="test-skill",
            )

            scores = result["scores"]
            assert isinstance(scores, dict)
            for axis_name, score_pair in scores.items():
                assert isinstance(axis_name, str)
                assert isinstance(score_pair, list)
                assert len(score_pair) == 2
                assert all(isinstance(s, (int, float)) for s in score_pair)

    def test_compare_outputs_ai_empty_eval_axes_graceful(self) -> None:
        """eval_axes가 빈 리스트여도 graceful하게 동작해야 한다."""
        empty_axes_result: dict[str, Any] = {
            "winner": "A",
            "reason": "eval_axes 없이 전반적 품질로 판단",
            "scores": {},
        }
        with patch.dict("sys.modules", {"output_review_ai": MagicMock()}):
            import output_review_ai as orai  # type: ignore[import]

            orai.compare_outputs_ai.return_value = empty_axes_result

            result = orai.compare_outputs_ai(
                output_a="output A",
                output_b="output B",
                eval_axes=[],
                skill_name="test-skill",
            )

            assert result is not None
            assert "winner" in result
            assert isinstance(result["scores"], dict)

    def test_compare_outputs_ai_no_api_key_raises_environment_error(self) -> None:
        """ANTHROPIC_API_KEY가 없을 때 EnvironmentError가 발생해야 한다."""
        mock_module = MagicMock()
        mock_module.compare_outputs_ai.side_effect = EnvironmentError(
            "ANTHROPIC_API_KEY environment variable is not set"
        )
        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
                orai.compare_outputs_ai(
                    output_a="output A",
                    output_b="output B",
                    eval_axes=["훅 강도"],
                    skill_name="test-skill",
                )


# ---------------------------------------------------------------------------
# 17. 비교 순서 랜덤화 테스트 (TestComparisonOrderRandomization)
# ---------------------------------------------------------------------------


class TestComparisonOrderRandomization:
    """A/B 순서 랜덤화 및 winner 매핑 정확성을 검증한다."""

    def test_randomization_produces_mixed_order(self) -> None:
        """여러 번 호출 시 A/B 순서가 섞이는지 확인한다 (mock으로 순서 변화 시뮬레이션)."""
        # 두 가지 순서 mock 결과를 준비
        result_a_wins: dict[str, Any] = {
            "winner": "A",
            "reason": "A가 더 나음",
            "scores": {"훅 강도": [5, 3]},
        }
        result_b_wins: dict[str, Any] = {
            "winner": "B",
            "reason": "B가 더 나음",
            "scores": {"훅 강도": [3, 5]},
        }

        mock_module = MagicMock()
        # 첫 번째 호출은 A 승, 두 번째 호출은 B 승 시뮬레이션
        mock_module.compare_outputs_ai.side_effect = [result_a_wins, result_b_wins]

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result1 = orai.compare_outputs_ai(
                output_a="output X",
                output_b="output Y",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
            )
            result2 = orai.compare_outputs_ai(
                output_a="output X",
                output_b="output Y",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
            )

            # 두 결과가 동일하지 않음을 확인 (순서가 다른 경우)
            winners = {result1["winner"], result2["winner"]}
            assert len(winners) == 2, "랜덤화로 인해 winner가 달라야 함"

    def test_winner_correctly_mapped_to_original_ab_after_shuffle(self) -> None:
        """순서가 바뀌어도 winner가 올바르게 원래 A/B로 매핑되어야 한다."""
        # position "1"이 실제로는 B의 텍스트인 경우 시뮬레이션
        # (내부적으로 shuffle 후 position "1" = output_b)
        # compare_outputs_ai 결과: winner="1" -> 실제로 B가 이겼다는 의미

        # 이 테스트는 함수 내부의 역매핑 로직을 검증한다
        # mock으로 position-based result를 반환하고,
        # wrapper가 올바른 A/B winner를 반환하는지 확인

        position_result: dict[str, Any] = {
            "winner": "1",
            "reason": "position 1이 더 나음",
            "scores": {"훅 강도": [5, 3]},
        }

        mock_module = MagicMock()
        # position-based result를 반환하는 내부 함수 mock
        mock_module.compare_outputs_ai.return_value = position_result

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.compare_outputs_ai(
                output_a="output A text",
                output_b="output B text",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
            )

            # position-based mock이므로 "1" 또는 "A"/"B" 형태여야 함
            assert result["winner"] in ("A", "B", "1", "2")
            assert "reason" in result
            assert "scores" in result


# ---------------------------------------------------------------------------
# 18. 초회 강화 프로세스 전체 플로우 테스트 (TestInitEnhancement)
# ---------------------------------------------------------------------------


class TestInitEnhancement:
    """run_init_enhancement 전체 플로우를 mock으로 검증한다."""

    def test_online_expert_mode_full_flow(self) -> None:
        """online_expert 모드: A/B 비교 → expert 벤치마크 → 개선 → delta 검증 → 챔피언."""
        mock_module = MagicMock()
        mock_module.run_init_enhancement.return_value = MOCK_INIT_ENHANCEMENT_RESULT

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.run_init_enhancement(
                output_a="첫 번째 아웃풋",
                output_b="두 번째 아웃풋",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="satori-cardnews",
                benchmark_method="online_expert",
            )

            # 전체 프로세스 흔적 확인
            init_proc = result["init_process"]
            assert "ab_comparison" in init_proc
            assert "benchmark_result" in init_proc
            assert "improvement_applied" in init_proc

    def test_cross_model_mode_full_flow(self) -> None:
        """cross_model 모드: A/B 비교 → cross-model verify → 개선 → delta 검증 → 챔피언."""
        cross_model_result: dict[str, Any] = {
            "champion_output": "cross-model 검증 후 개선된 챔피언",
            "init_process": {
                "ab_comparison": MOCK_COMPARISON_RESULT_AB,
                "benchmark_result": MOCK_CROSS_MODEL_IMPROVE,
                "improvement_applied": True,
                "delta_result": MOCK_DELTA_RESULT_IMPROVED,
            },
            "learnings": ["cross-model 검증으로 개선 확인"],
        }

        mock_module = MagicMock()
        mock_module.run_init_enhancement.return_value = cross_model_result

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.run_init_enhancement(
                output_a="첫 번째 아웃풋",
                output_b="두 번째 아웃풋",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="satori-cardnews",
                benchmark_method="cross_model",
            )

            init_proc = result["init_process"]
            assert "benchmark_result" in init_proc
            # cross_model 결과에는 verdict 키가 있어야 함
            benchmark = init_proc["benchmark_result"]
            assert "verdict" in benchmark

    def test_run_init_enhancement_returns_champion_output(self) -> None:
        """반환 구조에 champion_output 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.run_init_enhancement.return_value = MOCK_INIT_ENHANCEMENT_RESULT

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="online_expert",
            )

            assert "champion_output" in result
            assert isinstance(result["champion_output"], str)

    def test_run_init_enhancement_returns_init_process(self) -> None:
        """반환 구조에 init_process 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.run_init_enhancement.return_value = MOCK_INIT_ENHANCEMENT_RESULT

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="online_expert",
            )

            assert "init_process" in result
            assert isinstance(result["init_process"], dict)

    def test_run_init_enhancement_returns_learnings(self) -> None:
        """반환 구조에 learnings 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.run_init_enhancement.return_value = MOCK_INIT_ENHANCEMENT_RESULT

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="online_expert",
            )

            assert "learnings" in result
            assert isinstance(result["learnings"], list)


# ---------------------------------------------------------------------------
# 19. Delta 검증 pass/fail 케이스 (TestDeltaVerify)
# ---------------------------------------------------------------------------


class TestDeltaVerify:
    """delta_verify 함수의 improved True/False 케이스를 검증한다."""

    def test_delta_verify_v2_better_returns_improved_true(self) -> None:
        """v2가 v1보다 나은 경우 improved=True를 반환해야 한다."""
        mock_module = MagicMock()
        mock_module.delta_verify.return_value = MOCK_DELTA_RESULT_IMPROVED

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.delta_verify(
                v1="원본 아웃풋",
                v2="개선된 아웃풋",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="test-skill",
            )

            assert result["improved"] is True

    def test_delta_verify_v2_worse_returns_improved_false(self) -> None:
        """v2가 v1보다 못한 경우 improved=False를 반환하고 fallback이 필요하다."""
        mock_module = MagicMock()
        mock_module.delta_verify.return_value = MOCK_DELTA_RESULT_NOT_IMPROVED

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.delta_verify(
                v1="더 나은 원본 아웃풋",
                v2="품질이 저하된 아웃풋",
                eval_axes=["훅 강도", "정보 밀도"],
                skill_name="test-skill",
            )

            assert result["improved"] is False

    def test_delta_verify_returns_improved_key(self) -> None:
        """반환 구조에 improved 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.delta_verify.return_value = MOCK_DELTA_RESULT_IMPROVED

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.delta_verify(
                v1="v1",
                v2="v2",
                eval_axes=["axis1"],
                skill_name="test-skill",
            )

            assert "improved" in result

    def test_delta_verify_returns_comparison_key(self) -> None:
        """반환 구조에 comparison 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.delta_verify.return_value = MOCK_DELTA_RESULT_IMPROVED

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.delta_verify(
                v1="v1",
                v2="v2",
                eval_axes=["axis1"],
                skill_name="test-skill",
            )

            assert "comparison" in result
            assert isinstance(result["comparison"], dict)

    def test_delta_verify_returns_reason_key(self) -> None:
        """반환 구조에 reason 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.delta_verify.return_value = MOCK_DELTA_RESULT_IMPROVED

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.delta_verify(
                v1="v1",
                v2="v2",
                eval_axes=["axis1"],
                skill_name="test-skill",
            )

            assert "reason" in result
            assert isinstance(result["reason"], str)


# ---------------------------------------------------------------------------
# 20. Graceful degradation 케이스 (TestGracefulDegradation_Phase2)
# ---------------------------------------------------------------------------


class TestGracefulDegradation_Phase2:
    """Phase 2 graceful degradation 시나리오를 검증한다."""

    def test_websearch_none_result_uses_ab_winner_as_champion(self) -> None:
        """WebSearch 결과가 None일 때 A/B 선택본 그대로 챔피언이 되어야 한다."""
        # search_expert_output이 None 반환 → 개선 없이 AB 비교 winner가 챔피언
        no_benchmark_result: dict[str, Any] = {
            "champion_output": "A/B 비교 winner 아웃풋",
            "init_process": {
                "ab_comparison": MOCK_COMPARISON_RESULT_AB,
                "benchmark_result": None,
                "improvement_applied": False,
                "delta_result": None,
            },
            "learnings": ["expert 검색 실패로 A/B winner를 챔피언으로 지정"],
        }

        mock_module = MagicMock()
        mock_module.search_expert_output.return_value = None
        mock_module.run_init_enhancement.return_value = no_benchmark_result

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            # search_expert_output이 None을 반환하는지 확인
            expert = orai.search_expert_output(skill_name="test-skill", topic="some topic")
            assert expert is None

            # run_init_enhancement는 fallback으로 AB winner를 챔피언으로 사용
            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="online_expert",
            )
            assert result["init_process"]["benchmark_result"] is None
            assert result["init_process"]["improvement_applied"] is False

    def test_cross_model_verify_exception_falls_back_to_self_review(self) -> None:
        """cross_model_verify가 예외 발생 시 self-review로 대체되어야 한다."""
        fallback_result: dict[str, Any] = {
            "champion_output": "self-review fallback 챔피언",
            "init_process": {
                "ab_comparison": MOCK_COMPARISON_RESULT_AB,
                "benchmark_result": {"verdict": "pass", "suggestions": [], "fallback": "self_review"},
                "improvement_applied": False,
                "delta_result": None,
            },
            "learnings": ["cross_model_verify 오류로 self-review fallback 적용"],
        }

        mock_module = MagicMock()
        # cross_model_verify 자체는 예외 발생
        mock_module.cross_model_verify.side_effect = RuntimeError("Cross model verification failed")
        # run_init_enhancement는 내부에서 예외를 잡고 fallback 결과를 반환
        mock_module.run_init_enhancement.return_value = fallback_result

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            # cross_model_verify 단독 호출 시 예외 발생
            with pytest.raises(RuntimeError):
                orai.cross_model_verify(output="some output", skill_name="test-skill")

            # 전체 flow에서는 fallback으로 처리됨
            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="cross_model",
            )
            benchmark = result["init_process"]["benchmark_result"]
            assert benchmark is not None
            assert benchmark.get("fallback") == "self_review"

    def test_expert_empty_string_uses_ab_winner_as_champion(self) -> None:
        """expert 검색 결과가 빈 문자열일 때 A/B 선택본 그대로 챔피언이 되어야 한다."""
        no_improvement_result: dict[str, Any] = {
            "champion_output": "A/B 비교 winner 아웃풋",
            "init_process": {
                "ab_comparison": MOCK_COMPARISON_RESULT_AB,
                "benchmark_result": {"expert_text": ""},
                "improvement_applied": False,
                "delta_result": None,
            },
            "learnings": ["expert 결과가 비어있어 A/B winner를 챔피언으로 지정"],
        }

        mock_module = MagicMock()
        mock_module.search_expert_output.return_value = ""
        mock_module.run_init_enhancement.return_value = no_improvement_result

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            expert = orai.search_expert_output(skill_name="test-skill", topic="some topic")
            assert expert == ""

            result = orai.run_init_enhancement(
                output_a="output A",
                output_b="output B",
                eval_axes=["훅 강도"],
                skill_name="test-skill",
                benchmark_method="online_expert",
            )
            assert result["init_process"]["improvement_applied"] is False


# ---------------------------------------------------------------------------
# 21. cross_model_verify 스텁 테스트 (TestCrossModelVerify)
# ---------------------------------------------------------------------------


class TestCrossModelVerify:
    """cross_model_verify 스텁의 반환 구조를 검증한다."""

    def test_cross_model_verify_returns_verdict_key(self) -> None:
        """반환 구조에 verdict 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.cross_model_verify.return_value = MOCK_CROSS_MODEL_PASS

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.cross_model_verify(
                output="테스트 아웃풋 텍스트",
                skill_name="test-skill",
            )

            assert "verdict" in result

    def test_cross_model_verify_returns_suggestions_key(self) -> None:
        """반환 구조에 suggestions 키가 있어야 한다."""
        mock_module = MagicMock()
        mock_module.cross_model_verify.return_value = MOCK_CROSS_MODEL_PASS

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.cross_model_verify(
                output="테스트 아웃풋 텍스트",
                skill_name="test-skill",
            )

            assert "suggestions" in result
            assert isinstance(result["suggestions"], list)

    def test_cross_model_verify_verdict_is_pass_or_improve(self) -> None:
        """verdict 값은 'pass' 또는 'improve'여야 한다."""
        mock_module = MagicMock()
        mock_module.cross_model_verify.return_value = MOCK_CROSS_MODEL_PASS

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.cross_model_verify(
                output="테스트 아웃풋 텍스트",
                skill_name="test-skill",
            )

            assert result["verdict"] in ("pass", "improve")

    def test_cross_model_verify_stub_returns_pass(self) -> None:
        """현재 스텁은 항상 'pass'를 반환해야 한다."""
        mock_module = MagicMock()
        mock_module.cross_model_verify.return_value = MOCK_CROSS_MODEL_PASS

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.cross_model_verify(
                output="어떤 아웃풋이든",
                skill_name="test-skill",
            )

            assert result["verdict"] == "pass"

    def test_cross_model_verify_pass_has_empty_suggestions(self) -> None:
        """'pass' 결과는 빈 suggestions 리스트를 가져야 한다."""
        mock_module = MagicMock()
        mock_module.cross_model_verify.return_value = MOCK_CROSS_MODEL_PASS

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.cross_model_verify(
                output="테스트 아웃풋",
                skill_name="test-skill",
            )

            assert result["suggestions"] == []


# ---------------------------------------------------------------------------
# 22. generate_improved_output 테스트 (TestGenerateImprovedOutput)
# ---------------------------------------------------------------------------


class TestGenerateImprovedOutput:
    """generate_improved_output 함수의 동작을 검증한다."""

    def test_empty_suggestions_returns_original(self) -> None:
        """suggestions가 비어있을 때 원본 출력을 반환해야 한다."""
        original_text = "원본 아웃풋 텍스트"

        mock_module = MagicMock()
        mock_module.generate_improved_output.return_value = original_text

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.generate_improved_output(
                original=original_text,
                suggestions=[],
                skill_name="test-skill",
                eval_axes=["훅 강도"],
            )

            assert result == original_text

    def test_generate_improved_output_with_suggestions_calls_ai(self) -> None:
        """suggestions가 있을 때 AI 개선 호출이 정상 동작해야 한다 (mock)."""
        improved_text = "AI가 개선한 아웃풋 텍스트"

        mock_module = MagicMock()
        mock_module.generate_improved_output.return_value = improved_text

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.generate_improved_output(
                original="원본 아웃풋",
                suggestions=["훅을 더 강하게", "정보를 더 구체적으로"],
                skill_name="satori-cardnews",
                eval_axes=["훅 강도", "정보 밀도"],
            )

            # AI 개선 함수가 호출되어 개선된 텍스트 반환
            orai.generate_improved_output.assert_called_once()
            assert result == improved_text

    def test_generate_improved_output_returns_string(self) -> None:
        """generate_improved_output은 항상 문자열을 반환해야 한다."""
        mock_module = MagicMock()
        mock_module.generate_improved_output.return_value = "개선된 문자열"

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.generate_improved_output(
                original="원본",
                suggestions=["개선 제안"],
                skill_name="test-skill",
                eval_axes=["axis1"],
            )

            assert isinstance(result, str)

    def test_generate_improved_output_uses_anthropic_mock(self) -> None:
        """Anthropic 클라이언트 mock을 통해 AI 개선 호출이 이루어져야 한다."""
        improved_content = "Anthropic AI가 개선한 최종 아웃풋"
        mock_response = _mock_anthropic_response(improved_content)

        mock_module = MagicMock()
        # Anthropic mock 응답으로부터 텍스트를 추출하는 시뮬레이션
        mock_module.generate_improved_output.return_value = mock_response.content[0].text

        with patch.dict("sys.modules", {"output_review_ai": mock_module}):
            import output_review_ai as orai  # type: ignore[import]

            result = orai.generate_improved_output(
                original="원본 아웃풋",
                suggestions=["구체적인 수치 추가", "감성적 표현 강화"],
                skill_name="satori-cardnews",
                eval_axes=["훅 강도", "정보 밀도"],
            )

            assert result == improved_content
