"""Automatic test/lint verification loop.

Detects project test and lint commands, runs them after agent edits,
and feeds failures back to the agent for automatic fixing.
"""

from __future__ import annotations

import asyncio
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path

logger = logging.getLogger(__name__)


@dataclass
class VerifyConfig:
    """Configuration for the verification loop."""

    enabled: bool = False
    test_command: str | None = None
    lint_command: str | None = None
    max_retries: int = 3
    auto_detect: bool = True


@dataclass
class VerifyResult:
    """Result of a verification run."""

    test_passed: bool | None = None  # None = not run
    lint_passed: bool | None = None
    test_output: str = ""
    lint_output: str = ""
    errors: list[str] = field(default_factory=list)

    @property
    def all_passed(self) -> bool:
        """True if all executed checks passed."""
        if self.test_passed is False:
            return False
        if self.lint_passed is False:
            return False
        return True

    @property
    def summary(self) -> str:
        """Human-readable summary."""
        parts = []
        if self.test_passed is not None:
            parts.append(f"Tests: {'PASS' if self.test_passed else 'FAIL'}")
        if self.lint_passed is not None:
            parts.append(f"Lint: {'PASS' if self.lint_passed else 'FAIL'}")
        return " | ".join(parts) if parts else "No checks configured"


def detect_verify_commands(cwd: str | Path | None = None) -> VerifyConfig:
    """Auto-detect test and lint commands from project files.

    Checks pyproject.toml, package.json, Makefile, etc.

    Args:
        cwd: Working directory. Defaults to current directory.

    Returns:
        VerifyConfig with detected commands.
    """
    root = Path(cwd) if cwd else Path.cwd()
    config = VerifyConfig(auto_detect=True)

    # --- Test command detection ---

    # Makefile
    makefile = root / "Makefile"
    if makefile.exists():
        try:
            content = makefile.read_text()
            if "test:" in content or "tests:" in content:
                config.test_command = "make test"
        except (OSError, UnicodeDecodeError):
            pass

    # Python (pyproject.toml / pytest)
    pyproject = root / "pyproject.toml"
    if pyproject.exists() and not config.test_command:
        try:
            content = pyproject.read_text()
            if "[tool.pytest" in content or (root / "pytest.ini").exists():
                config.test_command = "pytest -x -q"
        except (OSError, UnicodeDecodeError):
            pass
        if not config.test_command and ((root / "tests").is_dir() or (root / "test").is_dir()):
            config.test_command = "pytest -x -q"

    # Node (package.json)
    pkg_json = root / "package.json"
    if pkg_json.exists() and not config.test_command:
        try:
            pkg = json.loads(pkg_json.read_text())
            scripts = pkg.get("scripts", {})
            if "test" in scripts:
                config.test_command = "npm test"
        except (OSError, UnicodeDecodeError, json.JSONDecodeError):
            pass

    # --- Lint command detection ---

    # Python (ruff / flake8)
    if pyproject.exists():
        try:
            content = pyproject.read_text()
            if "[tool.ruff" in content or (root / "ruff.toml").exists():
                config.lint_command = "ruff check --fix ."
            elif "[tool.flake8" in content or (root / ".flake8").exists():
                config.lint_command = "flake8 ."
        except (OSError, UnicodeDecodeError):
            pass

    # Node (eslint)
    if pkg_json.exists() and not config.lint_command:
        try:
            pkg = json.loads(pkg_json.read_text())
            scripts = pkg.get("scripts", {})
            if "lint" in scripts:
                config.lint_command = "npm run lint"
        except (OSError, UnicodeDecodeError, json.JSONDecodeError):
            pass

    return config


async def run_verify(
    config: VerifyConfig,
    cwd: str | Path | None = None,
    timeout: int = 120,
) -> VerifyResult:
    """Execute test and lint commands.

    Args:
        config: Verification configuration.
        cwd: Working directory.
        timeout: Max seconds per command.

    Returns:
        VerifyResult with outcomes.
    """
    result = VerifyResult()
    work_dir = str(cwd) if cwd else None

    # Run test command
    if config.test_command:
        test_out = await _run_command(config.test_command, work_dir, timeout)
        result.test_passed = test_out[0]
        result.test_output = test_out[1]
        if not result.test_passed:
            result.errors.extend(_parse_errors(test_out[1]))

    # Run lint command
    if config.lint_command:
        lint_out = await _run_command(config.lint_command, work_dir, timeout)
        result.lint_passed = lint_out[0]
        result.lint_output = lint_out[1]
        if not result.lint_passed:
            result.errors.extend(_parse_errors(lint_out[1]))

    return result


async def _run_command(
    command: str,
    cwd: str | None,
    timeout: int,
) -> tuple[bool, str]:
    """Run a shell command and return (success, output).

    Args:
        command: Shell command to run.
        cwd: Working directory.
        timeout: Max seconds.

    Returns:
        Tuple of (passed, combined_output).
    """
    try:
        proc = await asyncio.create_subprocess_shell(
            command,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
            cwd=cwd,
        )
        stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
        output = stdout.decode("utf-8", errors="replace")
        # Truncate long output
        if len(output) > 8000:
            output = output[:4000] + "\n...(truncated)...\n" + output[-4000:]
        return (proc.returncode == 0, output)
    except asyncio.TimeoutError:
        return (False, f"Command timed out after {timeout}s: {command}")
    except Exception as e:
        return (False, f"Command failed: {e}")


def _parse_errors(output: str) -> list[str]:
    """Extract key error lines from command output.

    Args:
        output: Raw command output.

    Returns:
        List of error summary lines (max 10).
    """
    errors = []
    for line in output.splitlines():
        lower = line.lower()
        if any(kw in lower for kw in ("error", "fail", "assert", "traceback")):
            stripped = line.strip()
            if stripped and len(stripped) < 300:
                errors.append(stripped)
    return errors[:10]


def build_feedback_message(result: VerifyResult) -> str:
    """Build a feedback message for the agent from verification failures.

    Args:
        result: The verification result.

    Returns:
        Message string to send back to the agent.
    """
    parts = ["## Verification Failed\n"]

    if result.test_passed is False:
        parts.append("### Test Output\n```\n")
        parts.append(result.test_output[:4000])
        parts.append("\n```\n")

    if result.lint_passed is False:
        parts.append("### Lint Output\n```\n")
        parts.append(result.lint_output[:4000])
        parts.append("\n```\n")

    parts.append("Please fix the errors above and try again.")
    return "".join(parts)
