"""验证循环 - 执行后运行项目测试验证结果

遵循 Codex Cloud 模式：execute → verify → retry on failure。
默认验证命令：pytest, ruff check。
"""

from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass, field
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class VerificationResult:
    """验证循环运行结果"""

    passed: bool
    attempts: int
    test_output: str
    errors: list[str] = field(default_factory=list)


class VerificationLoop:
    """执行后运行项目测试验证结果

    遵循 Codex Cloud 模式：execute → verify → retry on failure。
    默认验证命令：pytest, ruff check。
    """

    def __init__(
        self,
        commands: list[str] | None = None,
        max_retries: int = 2,
        working_dir: str | None = None,
        timeout: float = 60.0,
    ):
        """
        Args:
            commands: 验证用的 Shell 命令列表。
                      默认: ["pytest -x -q", "ruff check src/"]
            max_retries: 初始执行后的最大重试次数。
            working_dir: 运行命令的工作目录。
            timeout: 每个验证命令的超时时间（秒）。
        """
        self._commands = commands or ["pytest -x -q", "ruff check src/"]
        self._max_retries = max_retries
        self._working_dir = working_dir
        self._timeout = timeout

    async def verify(self) -> VerificationResult:
        """运行验证命令并返回结果

        依次执行每个命令，捕获 stdout/stderr。
        如果任何命令失败（非零退出码），标记为失败。

        Returns:
            VerificationResult，所有命令成功时 passed=True。
        """
        all_output: list[str] = []
        errors: list[str] = []
        passed = True

        for cmd in self._commands:
            try:
                proc = await asyncio.create_subprocess_shell(
                    cmd,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.STDOUT,
                    cwd=self._working_dir,
                )
                stdout, _ = await asyncio.wait_for(
                    proc.communicate(),
                    timeout=self._timeout,
                )
            except asyncio.TimeoutError:
                try:
                    proc.kill()
                except ProcessLookupError:
                    pass
                await proc.wait()
                output = f"Command timed out after {self._timeout}s: {cmd}"
                all_output.append(output)
                errors.append(output)
                passed = False
                continue
            except Exception as e:
                output = f"Command failed to execute: {cmd}: {e}"
                all_output.append(output)
                errors.append(output)
                passed = False
                continue

            output = stdout.decode("utf-8", errors="replace") if stdout else ""
            all_output.append(f"$ {cmd}\n{output}")

            if proc.returncode != 0:
                passed = False
                errors.append(f"Command failed (exit {proc.returncode}): {cmd}\n{output}")

        return VerificationResult(
            passed=passed,
            attempts=1,
            test_output="\n".join(all_output),
            errors=errors,
        )

    async def verify_and_retry(
        self,
        fix_callback: Any = None,
    ) -> VerificationResult:
        """运行验证，如果失败则调用 fix_callback 并重试

        Args:
            fix_callback: 异步可调用对象，接收 (errors, test_output)，
                          尝试修复问题。在重试之间调用。

        Returns:
            VerificationResult，包含最终验证状态。
        """
        result = await self.verify()

        retries = 0
        while not result.passed and retries < self._max_retries:
            retries += 1
            logger.info(
                "Verification failed (attempt %d/%d), %s",
                retries,
                self._max_retries,
                "calling fix_callback" if fix_callback else "retrying",
            )

            if fix_callback is not None:
                try:
                    await fix_callback(result.errors, result.test_output)
                except Exception as e:
                    logger.warning("fix_callback raised an error: %s", e)

            result = await self.verify()
            result.attempts = retries + 1

        return result
