"""tests/test_engine_v2_phase5.py — Phase 5 TDD 테스트 스위트.

G13+G15: ChapterRunner, QCHook 검증.
작성 순서: 테스트 먼저(RED), 구현 후 GREEN 확인.
"""

from __future__ import annotations

import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from engine_v2.engine_result import EngineResult

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


def _make_result(
    content: str = "정상 응답",
    engine: str = "claude",
    error: bool = False,
    task_id: str = "task-001",
    step: int = 1,
    flagged_count: int = 0,
    token_est: int = 10,
) -> EngineResult:
    """테스트용 EngineResult 생성."""
    return EngineResult(
        engine=engine,  # type: ignore[arg-type]
        content=content,
        clean=content,
        task_id=task_id,
        step=step,
        error=error,
        flagged_count=flagged_count,
        token_est=token_est,
        timestamp=datetime(2026, 3, 18, 12, 0, 0, tzinfo=timezone.utc),
    )


# ---------------------------------------------------------------------------
# G13: ChapterRunner 테스트
# ---------------------------------------------------------------------------


class TestChapterRunnerOutputDir:
    """출력 디렉토리 생성 확인."""

    @pytest.mark.asyncio
    async def test_chapter_runner_creates_output_dir(self, tmp_path: Path) -> None:
        """run() 호출 시 chapter-N 디렉토리가 생성되어야 한다."""
        from publishing.chapter_runner import ChapterRunner

        mock_result_ok = _make_result(content="초안 내용")
        mock_result_gemini = _make_result(content="Gemini 초안", engine="gemini")
        mock_step2 = _make_result(content="집대성 결과", engine="gemini")
        mock_step3 = _make_result(content="수정 불필요. 우수합니다.", engine="codex")
        mock_step5 = _make_result(content="최종 결과", engine="claude")

        async def fake_run_step(prompts, engines, task_id, step, timeout=900) -> list[EngineResult]:
            if step == 1:
                return [mock_result_ok, mock_result_gemini]
            elif step == 2:
                return [mock_step2]
            elif step == 3:
                return [mock_step3]
            elif step == 5:
                return [mock_step5]
            return [mock_result_ok]

        runner = ChapterRunner(chapter=99, task_id="task-test", guide="가이드", chapter_info="챕터 정보")

        # _OUTPUT_DIR을 tmp_path로 오버라이드
        with patch("publishing.chapter_runner._OUTPUT_DIR", tmp_path):
            with patch.object(runner._adapter, "run_step", side_effect=fake_run_step):
                await runner.run()

        chapter_dir = tmp_path / "chapter-99"
        assert chapter_dir.exists()
        assert chapter_dir.is_dir()


class TestChapterRunnerStep1Parallel:
    """Step 1이 claude+gemini 병렬 호출 확인."""

    @pytest.mark.asyncio
    async def test_chapter_runner_step1_parallel(self, tmp_path: Path) -> None:
        """Step 1은 claude와 gemini를 동시에 호출해야 한다."""
        from publishing.chapter_runner import ChapterRunner

        captured_calls: list[dict] = []

        async def fake_run_step(prompts, engines, task_id, step, timeout=900) -> list[EngineResult]:
            captured_calls.append({"step": step, "engines": list(engines)})
            if step == 1:
                return [
                    _make_result(content="Claude 초안", engine="claude"),
                    _make_result(content="Gemini 초안", engine="gemini"),
                ]
            elif step == 2:
                return [_make_result(content="집대성", engine="gemini")]
            elif step == 3:
                return [_make_result(content="수정 불필요. 충분합니다.", engine="codex")]
            elif step == 5:
                return [_make_result(content="최종", engine="claude")]
            return [_make_result()]

        runner = ChapterRunner(chapter=1, task_id="task-step1", guide="", chapter_info="")

        with patch("publishing.chapter_runner._OUTPUT_DIR", tmp_path):
            with patch.object(runner._adapter, "run_step", side_effect=fake_run_step):
                await runner.run()

        # Step 1 호출 확인
        step1_calls = [c for c in captured_calls if c["step"] == 1]
        assert len(step1_calls) == 1
        assert "claude" in step1_calls[0]["engines"]
        assert "gemini" in step1_calls[0]["engines"]


class TestChapterRunnerStep5ClaudeFinal:
    """Step 5가 claude 엔진으로 호출 확인."""

    @pytest.mark.asyncio
    async def test_chapter_runner_step5_claude_final(self, tmp_path: Path) -> None:
        """Step 5는 반드시 claude 엔진을 사용해야 한다."""
        from publishing.chapter_runner import ChapterRunner

        captured_calls: list[dict] = []

        async def fake_run_step(prompts, engines, task_id, step, timeout=900) -> list[EngineResult]:
            captured_calls.append({"step": step, "engines": list(engines)})
            if step == 1:
                return [
                    _make_result(content="초안1", engine="claude"),
                    _make_result(content="초안2", engine="gemini"),
                ]
            elif step == 2:
                return [_make_result(content="집대성", engine="gemini")]
            elif step == 3:
                return [_make_result(content="수정 불필요. 충분합니다.", engine="codex")]
            elif step == 5:
                return [_make_result(content="최종 통합", engine="claude")]
            return [_make_result()]

        runner = ChapterRunner(chapter=2, task_id="task-step5", guide="", chapter_info="")

        with patch("publishing.chapter_runner._OUTPUT_DIR", tmp_path):
            with patch.object(runner._adapter, "run_step", side_effect=fake_run_step):
                await runner.run()

        step5_calls = [c for c in captured_calls if c["step"] == 5]
        assert len(step5_calls) == 1
        assert step5_calls[0]["engines"] == ["claude"]


class TestChapterRunnerConsensusStopsAtMax:
    """최대 3라운드 후 종료 확인."""

    @pytest.mark.asyncio
    async def test_chapter_runner_consensus_stops_at_max(self, tmp_path: Path) -> None:
        """Step 3-4 반복이 MAX_CONSENSUS_ROUNDS(3) 이하로 수행되어야 한다."""
        from publishing.chapter_runner import ChapterRunner
        from publishing.consensus_pipeline import MAX_CONSENSUS_ROUNDS

        step3_call_count = 0

        async def fake_run_step(prompts, engines, task_id, step, timeout=900) -> list[EngineResult]:
            nonlocal step3_call_count
            if step == 1:
                return [
                    _make_result(content="초안1", engine="claude"),
                    _make_result(content="초안2", engine="gemini"),
                ]
            elif step == 2:
                return [_make_result(content="집대성", engine="gemini")]
            elif step == 3:
                step3_call_count += 1
                # 항상 "수정 필요" → 합의 미달 → 최대 라운드까지 반복
                return [_make_result(content="수정 필요. 오류가 있습니다.", engine="codex")]
            elif step == 4:
                return [_make_result(content="반영된 내용", engine="gemini")]
            elif step == 5:
                return [_make_result(content="최종", engine="claude")]
            return [_make_result()]

        runner = ChapterRunner(chapter=3, task_id="task-consensus", guide="", chapter_info="")

        with patch("publishing.chapter_runner._OUTPUT_DIR", tmp_path):
            with patch.object(runner._adapter, "run_step", side_effect=fake_run_step):
                await runner.run()

        # Step 3는 MAX_CONSENSUS_ROUNDS 이하로 호출되어야 함
        assert step3_call_count <= MAX_CONSENSUS_ROUNDS
        assert step3_call_count >= 1  # 최소 1번은 호출


class TestParseArgs:
    """CLI 인수 파싱 확인."""

    def test_parse_args_required(self) -> None:
        """--chapter와 --task-id가 필수 인수로 파싱되어야 한다."""
        from publishing.chapter_runner import parse_args

        args = parse_args(["--chapter", "5", "--task-id", "task-007"])
        assert args.chapter == 5
        assert args.task_id == "task-007"
        assert args.guide == ""
        assert args.chapter_info == ""

    def test_parse_args_optional(self) -> None:
        """--guide와 --chapter-info 선택 인수가 파싱되어야 한다."""
        from publishing.chapter_runner import parse_args

        args = parse_args(
            [
                "--chapter",
                "3",
                "--task-id",
                "task-003",
                "--guide",
                "집필가이드 내용",
                "--chapter-info",
                "챕터 3 정보",
            ]
        )
        assert args.chapter == 3
        assert args.task_id == "task-003"
        assert args.guide == "집필가이드 내용"
        assert args.chapter_info == "챕터 3 정보"

    def test_parse_args_missing_chapter_raises(self) -> None:
        """--chapter가 없으면 SystemExit이 발생해야 한다."""
        from publishing.chapter_runner import parse_args

        with pytest.raises(SystemExit):
            parse_args(["--task-id", "task-001"])

    def test_parse_args_missing_task_id_raises(self) -> None:
        """--task-id가 없으면 SystemExit이 발생해야 한다."""
        from publishing.chapter_runner import parse_args

        with pytest.raises(SystemExit):
            parse_args(["--chapter", "1"])


# ---------------------------------------------------------------------------
# G15: QCHook 테스트
# ---------------------------------------------------------------------------


class TestQCHookOnEngineComplete:
    """on_engine_complete() 파일 생성 확인."""

    def test_qc_hook_on_engine_complete(self, tmp_path: Path) -> None:
        """on_engine_complete() 호출 시 JSON 파일이 생성되어야 한다."""
        from engine_v2.qc_hook import FileQCHook

        hook = FileQCHook(output_dir=tmp_path)
        result = _make_result(
            content="엔진 응답",
            engine="claude",
            task_id="task-qc-001",
            step=1,
            flagged_count=0,
            token_est=42,
        )

        hook.on_engine_complete(result)

        expected_file = tmp_path / "task-qc-001_step1_claude.json"
        assert expected_file.exists()

        data = json.loads(expected_file.read_text(encoding="utf-8"))
        assert data["engine"] == "claude"
        assert data["task_id"] == "task-qc-001"
        assert data["step"] == 1
        assert data["error"] is False
        assert data["flagged_count"] == 0
        assert data["token_est"] == 42
        assert "timestamp" in data

    def test_qc_hook_on_engine_complete_error_result(self, tmp_path: Path) -> None:
        """에러 결과도 JSON 파일로 기록되어야 한다."""
        from engine_v2.qc_hook import FileQCHook

        hook = FileQCHook(output_dir=tmp_path)
        result = _make_result(
            content="",
            engine="gemini",
            error=True,
            task_id="task-qc-err",
            step=3,
        )

        hook.on_engine_complete(result)

        expected_file = tmp_path / "task-qc-err_step3_gemini.json"
        assert expected_file.exists()

        data = json.loads(expected_file.read_text(encoding="utf-8"))
        assert data["error"] is True
        assert data["engine"] == "gemini"


class TestQCHookOnPipelineComplete:
    """on_pipeline_complete() summary.json 생성 확인."""

    def test_qc_hook_on_pipeline_complete(self, tmp_path: Path) -> None:
        """on_pipeline_complete() 호출 시 _summary.json이 생성되어야 한다."""
        from engine_v2.qc_hook import FileQCHook

        hook = FileQCHook(output_dir=tmp_path)
        results = [
            _make_result(content="결과1", engine="claude", task_id="task-pipe-001", step=1),
            _make_result(content="결과2", engine="gemini", task_id="task-pipe-001", step=2),
            _make_result(content="", engine="codex", task_id="task-pipe-001", step=3, error=True),
        ]

        hook.on_pipeline_complete(results)

        expected_file = tmp_path / "task-pipe-001_summary.json"
        assert expected_file.exists()

        data = json.loads(expected_file.read_text(encoding="utf-8"))
        assert data["task_id"] == "task-pipe-001"
        assert data["total_steps"] == 3
        assert data["errors"] == 1
        assert "claude" in data["engines_used"]
        assert "gemini" in data["engines_used"]
        assert "codex" in data["engines_used"]

    def test_qc_hook_on_pipeline_complete_empty(self, tmp_path: Path) -> None:
        """빈 결과 목록에서 on_pipeline_complete()가 파일을 생성하지 않아야 한다."""
        from engine_v2.qc_hook import FileQCHook

        hook = FileQCHook(output_dir=tmp_path)
        hook.on_pipeline_complete([])

        # 빈 목록이면 아무 파일도 생성되지 않아야 함
        assert list(tmp_path.iterdir()) == []

    def test_qc_hook_on_pipeline_complete_flagged_total(self, tmp_path: Path) -> None:
        """total_flagged가 flagged_count 합계로 기록되어야 한다."""
        from engine_v2.qc_hook import FileQCHook

        hook = FileQCHook(output_dir=tmp_path)
        results = [
            _make_result(content="결과1", engine="claude", task_id="task-flag", step=1, flagged_count=2),
            _make_result(content="결과2", engine="gemini", task_id="task-flag", step=2, flagged_count=3),
        ]

        hook.on_pipeline_complete(results)

        data = json.loads((tmp_path / "task-flag_summary.json").read_text(encoding="utf-8"))
        assert data["total_flagged"] == 5


class TestQCHandlerProtocol:
    """QCHandler Protocol 인터페이스 적합성 확인."""

    def test_qc_handler_protocol_file_hook(self, tmp_path: Path) -> None:
        """FileQCHook이 QCHandler Protocol을 충족해야 한다."""
        from engine_v2.qc_hook import FileQCHook, QCHandler

        hook = FileQCHook(output_dir=tmp_path)

        # Protocol 메서드가 존재하는지 확인
        assert hasattr(hook, "on_engine_complete")
        assert hasattr(hook, "on_pipeline_complete")
        assert callable(hook.on_engine_complete)
        assert callable(hook.on_pipeline_complete)

    def test_qc_handler_protocol_custom_implementation(self, tmp_path: Path) -> None:
        """커스텀 QCHandler 구현이 Protocol을 충족해야 한다."""
        from engine_v2.qc_hook import QCHandler

        class CustomQCHandler:
            """커스텀 QC 핸들러."""

            def __init__(self) -> None:
                self.completed: list[EngineResult] = []
                self.pipeline_done: bool = False

            def on_engine_complete(self, result: EngineResult) -> None:
                self.completed.append(result)

            def on_pipeline_complete(self, results: list[EngineResult]) -> None:
                self.pipeline_done = True

        custom = CustomQCHandler()
        result = _make_result()
        custom.on_engine_complete(result)
        custom.on_pipeline_complete([result])

        assert len(custom.completed) == 1
        assert custom.pipeline_done is True

    def test_qc_hook_creates_output_dir_automatically(self, tmp_path: Path) -> None:
        """FileQCHook이 output_dir을 자동으로 생성해야 한다."""
        from engine_v2.qc_hook import FileQCHook

        new_dir = tmp_path / "nested" / "qc"
        assert not new_dir.exists()

        hook = FileQCHook(output_dir=new_dir)
        assert new_dir.exists()
