"""tests/test_cli_runner.py — CLIRunner TDD 테스트 스위트.

작성 순서: 테스트 먼저(RED), 구현 후 GREEN 확인.
"""

from __future__ import annotations

import asyncio
import os
import sys

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

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# 헬퍼: asyncio.subprocess.Process 목 생성
# ---------------------------------------------------------------------------


def _make_mock_process(
    stdout_data: bytes = b"",
    stderr_data: bytes = b"",
    returncode: int = 0,
) -> MagicMock:
    """asyncio.subprocess.Process를 모킹하는 객체를 반환한다."""
    proc = MagicMock()
    proc.returncode = returncode
    proc.communicate = AsyncMock(return_value=(stdout_data, stderr_data))
    proc.kill = MagicMock()
    return proc


def _make_timeout_process() -> MagicMock:
    """communicate()에서 asyncio.TimeoutError를 발생시키는 목 프로세스를 반환한다."""
    proc = MagicMock()
    proc.returncode = None
    proc.communicate = AsyncMock(side_effect=asyncio.TimeoutError())
    proc.kill = MagicMock()
    return proc


# ---------------------------------------------------------------------------
# test_run_claude_success
# ---------------------------------------------------------------------------


class TestRunClaude:
    """CLIRunner.run_claude() 검증."""

    @pytest.mark.asyncio
    async def test_run_claude_success(self) -> None:
        """Claude CLI 성공 시 stdout 텍스트를 CLIResult.stdout으로 반환해야 한다."""
        mock_proc = _make_mock_process(
            stdout_data=b"Claude response text",
            stderr_data=b"",
            returncode=0,
        )

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_claude("안녕하세요")

        assert result.stdout == "Claude response text"
        assert result.returncode == 0
        assert result.engine == "claude"
        assert result.timed_out is False

    @pytest.mark.asyncio
    async def test_run_claude_timeout(self) -> None:
        """타임아웃 시 timed_out=True이고 returncode=-1이어야 한다."""
        mock_proc = _make_timeout_process()

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_claude("타임아웃 테스트", timeout=1)

        assert result.timed_out is True
        assert result.returncode == -1
        assert result.engine == "claude"
        assert "Timeout" in result.stderr or "timeout" in result.stderr.lower()


# ---------------------------------------------------------------------------
# test_run_codex_*
# ---------------------------------------------------------------------------


class TestRunCodex:
    """CLIRunner.run_codex() 검증."""

    @pytest.mark.asyncio
    async def test_run_codex_fallback(self) -> None:
        """gpt-5.2-codex 요청 시 gpt-5.1-codex-mini로 치환되고 fallback_used=True여야 한다."""
        mock_proc = _make_mock_process(stdout_data=b"codex response", stderr_data=b"")

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_codex("테스트", model="gpt-5.2-codex")

        assert result.fallback_used is True
        assert result.engine == "codex"
        # cmd에 실제 사용된 모델이 gpt-5.1-codex-mini인지 확인
        call_args = list(mock_exec.call_args.args)
        cmd_str = " ".join(call_args)
        assert "gpt-5.1-codex-mini" in cmd_str
        assert "gpt-5.2-codex" not in cmd_str

    @pytest.mark.asyncio
    async def test_run_codex_no_fallback(self) -> None:
        """gpt-5.1-codex-mini 요청 시 치환 없이 fallback_used=False여야 한다."""
        mock_proc = _make_mock_process(stdout_data=b"mini response", stderr_data=b"")

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_codex("테스트", model="gpt-5.1-codex-mini")

        assert result.fallback_used is False
        assert result.engine == "codex"
        call_args = list(mock_exec.call_args.args)
        cmd_str = " ".join(call_args)
        assert "gpt-5.1-codex-mini" in cmd_str

    @pytest.mark.asyncio
    async def test_run_codex_skip_git_flag(self) -> None:
        """cmd에 --skip-git-repo-check 플래그가 반드시 포함되어야 한다."""
        mock_proc = _make_mock_process(stdout_data=b"ok", stderr_data=b"")

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
            from engine_v2.cli_runner import CLIRunner

            await CLIRunner.run_codex("플래그 확인")

        call_args = list(mock_exec.call_args.args)
        assert "--skip-git-repo-check" in call_args

    @pytest.mark.asyncio
    async def test_run_codex_gpt51_fallback(self) -> None:
        """gpt-5.1-codex 요청 시 gpt-5.1-codex-mini로 치환되고 fallback_used=True여야 한다."""
        mock_proc = _make_mock_process(stdout_data=b"ok", stderr_data=b"")

        with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_codex("테스트", model="gpt-5.1-codex")

        assert result.fallback_used is True
        call_args = list(mock_exec.call_args.args)
        cmd_str = " ".join(call_args)
        assert "gpt-5.1-codex-mini" in cmd_str


# ---------------------------------------------------------------------------
# test_exec_file_not_found
# ---------------------------------------------------------------------------


class TestExecFileNotFound:
    """_exec() FileNotFoundError 처리 검증."""

    @pytest.mark.asyncio
    async def test_exec_file_not_found(self) -> None:
        """명령어가 PATH에 없을 때 returncode=-1이고 stderr에 오류 내용이 있어야 한다."""
        with patch(
            "asyncio.create_subprocess_exec",
            side_effect=FileNotFoundError(),
        ):
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_claude("명령어 없음")

        assert result.returncode == -1
        assert result.engine == "claude"
        assert "not found" in result.stderr.lower() or len(result.stderr) > 0

    @pytest.mark.asyncio
    async def test_exec_codex_file_not_found(self) -> None:
        """codex 명령어가 PATH에 없을 때 returncode=-1이어야 한다."""
        with patch(
            "asyncio.create_subprocess_exec",
            side_effect=FileNotFoundError(),
        ):
            from engine_v2.cli_runner import CLIRunner

            result = await CLIRunner.run_codex("명령어 없음")

        assert result.returncode == -1
        assert result.engine == "codex"
