"""Shared utilities for M1-M6 E2E verification helpers.

Provides subprocess-based CLI invocation, workspace setup, and
common assertions for real (non-mocked) end-to-end tests.
"""

from __future__ import annotations

import contextlib
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


def run_cli(
    *args: str,
    workspace: str,
    env_extra: dict[str, str] | None = None,
    timeout: int = 120,
) -> subprocess.CompletedProcess[str]:
    """Run ``python -m cleveragents <args>`` in a subprocess.

    Uses the same Python interpreter as the running process.
    Inherits ``CLEVERAGENTS_HOME``, ``CLEVERAGENTS_AUTO_APPLY_MIGRATIONS``,
    and ``CLEVERAGENTS_TESTING_USE_MOCK_AI`` from the environment (set by
    ``robot/common.resource`` ``Setup Test Environment``).

    Parameters
    ----------
    *args:
        CLI arguments (e.g. ``"action", "create", "--config", path``).
    workspace:
        Working directory for the subprocess.
    env_extra:
        Additional environment variables to merge.
    timeout:
        Subprocess timeout in seconds.
    """
    env = os.environ.copy()
    # Ensure test-critical variables are set
    env.setdefault("CLEVERAGENTS_AUTO_APPLY_MIGRATIONS", "true")
    env.setdefault("CLEVERAGENTS_TESTING_USE_MOCK_AI", "true")
    env.setdefault("NO_COLOR", "1")
    if env_extra:
        env.update(env_extra)
    return subprocess.run(
        [sys.executable, "-m", "cleveragents", *args],
        cwd=workspace,
        capture_output=True,
        text=True,
        timeout=timeout,
        env=env,
    )


def run_cli_json(
    *args: str,
    workspace: str,
    env_extra: dict[str, str] | None = None,
) -> tuple[subprocess.CompletedProcess[str], dict | list | None]:
    """Run CLI command with ``--format json`` and parse output.

    Returns the completed process and parsed JSON (or ``None``).
    """
    full_args = [*args, "--format", "json"]
    result = run_cli(*full_args, workspace=workspace, env_extra=env_extra)
    parsed: dict | list | None = None
    if result.returncode == 0 and result.stdout.strip():
        with contextlib.suppress(json.JSONDecodeError):
            parsed = json.loads(result.stdout)
    return result, parsed


def setup_workspace(prefix: str = "e2e_") -> str:
    """Create an isolated workspace directory with a ready database.

    Sets ``CLEVERAGENTS_HOME`` and ``CLEVERAGENTS_DATABASE_URL`` to the
    workspace so all tables are available for every CLI command.

    When a pre-migrated template database is available (either via the
    ``CLEVERAGENTS_TEMPLATE_DB`` environment variable or at the default
    ``build/.template-migrated.db`` path), the template is copied
    instead of running full Alembic migrations.  This reduces per-test
    setup from ~1-3 s to ~1 ms, which is critical for parallel
    execution under pabot where many workers set up workspaces
    concurrently.

    Returns the absolute path to the workspace.
    """
    workspace = tempfile.mkdtemp(prefix=prefix)
    os.environ["CLEVERAGENTS_HOME"] = workspace
    db_path = os.path.join(workspace, "cleveragents_e2e.db")
    db_url = f"sqlite:///{db_path}"
    os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url

    # Fast path: copy pre-migrated template DB instead of running
    # 25+ Alembic migrations (avoids I/O contention under pabot).
    template = os.environ.get("CLEVERAGENTS_TEMPLATE_DB")
    if not template:
        default_template = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "build",
            ".template-migrated.db",
        )
        if os.path.isfile(default_template):
            template = default_template

    if template and os.path.isfile(template):
        shutil.copy2(template, db_path)
    else:
        # Fallback: run Alembic migrations (slow path).
        from cleveragents.infrastructure.database.migration_runner import (
            MigrationRunner,
        )

        runner = MigrationRunner(db_url)
        runner.init_or_upgrade(require_confirmation=False)

    return workspace


def cleanup_workspace(workspace: str) -> None:
    """Remove a workspace directory and unset env vars, ignoring errors."""
    shutil.rmtree(workspace, ignore_errors=True)
    os.environ.pop("CLEVERAGENTS_HOME", None)
    os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)


def fail(msg: str) -> None:
    """Print failure message and exit with code 1."""
    print(f"FAIL: {msg}", file=sys.stderr)
    raise SystemExit(1)


def is_expected_provider_unavailable(output: str) -> bool:
    """Return True when output matches known provider-unavailable failures."""
    hay = output.lower()
    patterns = (
        "provider openai is not configured",
        "provider 'openai' is not configured",
        "openai_api_key is not set",
        "openai_api_key not found",
        "no provider configured",
    )
    return any(pattern in hay for pattern in patterns)


def write_yaml(content: str) -> str:
    """Write YAML content to a temporary file and return its path."""
    fd, path = tempfile.mkstemp(suffix=".yaml")
    with os.fdopen(fd, "w") as fh:
        fh.write(content)
    return path


def init_bare_git_repo() -> str:
    """Create a bare git repository with an initial commit.

    Returns the path to the repository.
    """
    repo_dir = tempfile.mkdtemp(prefix="e2e_git_")
    cmds = [
        ["git", "init"],
        ["git", "config", "user.email", "test@example.com"],
        ["git", "config", "user.name", "Test"],
    ]
    for cmd in cmds:
        subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)

    readme = Path(repo_dir) / "README.md"
    readme.write_text("# Test repo\n")
    subprocess.run(
        ["git", "add", "."],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "commit", "-m", "Initial commit"],
        cwd=repo_dir,
        capture_output=True,
        check=True,
    )
    return repo_dir
