import json
import os
import sys
from pathlib import Path

import nox

# Global configuration
DEFAULT_PYTHON = "3.13"
SUPPORTED_PYTHONS = ["3.13"]
nox.options.reuse_existing_virtualenvs = True
nox.options.error_on_external_run = True

BEHAVE_PARALLEL_VERSION = "2.0.0"


def _default_processes() -> int:
    env_override = os.environ.get("TEST_PROCESSES")
    if env_override:
        return int(env_override)
    try:
        cpus = len(os.sched_getaffinity(0)) or 1
    except AttributeError:
        cpus = os.cpu_count() or 1
    # Keep default parallelism conservative to avoid timeout/OOM flakes
    # under heavy Robot/pabot subprocess fan-out in CI and shared runners.
    # Callers can still override with TEST_PROCESSES / --processes.
    return cpus


def _behave_parallel_args(posargs: list[str]) -> list[str]:
    has_custom_processes = any(
        arg in {"--processes", "-j"} or arg.startswith(("--processes=", "-j"))
        for arg in posargs
    )
    if has_custom_processes:
        return []
    return ["--processes", str(_default_processes())]


def _pabot_parallel_args(posargs: list[str]) -> list[str]:
    has_custom_processes = any(
        arg in {"--processes"} or arg.startswith("--processes=") for arg in posargs
    )
    if has_custom_processes:
        return []
    return ["--processes", str(_default_processes())]


def _split_pabot_args(posargs: list[str]) -> tuple[list[str], list[str]]:
    pabot_args: list[str] = []
    robot_args: list[str] = []
    i = 0
    while i < len(posargs):
        arg = posargs[i]
        if arg == "--processes" and i + 1 < len(posargs):
            pabot_args.extend([arg, posargs[i + 1]])
            i += 2
            continue
        if arg.startswith("--processes="):
            pabot_args.append(arg)
            i += 1
            continue
        robot_args.append(arg)
        i += 1
    return pabot_args, robot_args


def _create_template_db(session: nox.Session) -> str:
    """Build the pre-migrated SQLite template and return its path.

    Runs ``scripts/create_template_db.py`` to produce a SQLite file with all
    tables already created via ``Base.metadata.create_all()`` and the
    alembic_version table stamped at HEAD.  Each test scenario can then
    ``shutil.copy`` this file instead of running 25 Alembic migrations.
    """
    template_path = str(Path("build/.template-migrated.db").resolve())
    session.run(
        "python",
        "scripts/create_template_db.py",
        template_path,
        silent=True,
    )
    return template_path


def _install_behave_parallel(session: nox.Session) -> None:
    """Install behave-parallel with an in-process parallel runner.

    Instead of spawning one subprocess per feature file (the old model),
    the new CLI runs features in-process via behave's Python API.  Step
    definitions and environment hooks are loaded once; each feature file
    is then parsed and executed without interpreter startup overhead.

    Parallel execution uses ``multiprocessing.Pool`` with the ``fork``
    start method so that already-imported modules are shared (copy-on-write)
    across workers.

    The runner script is read from ``scripts/run_behave_parallel.py`` so
    that the noxfile stays concise and the script can be linted and typed
    independently.
    """
    tmp_dir = Path(session.create_tmp())
    source_dir = tmp_dir / "behave-parallel-inprocess"
    source_dir.mkdir(parents=True, exist_ok=True)

    pkg_dir = source_dir / "behave_parallel"
    pkg_dir.mkdir(parents=True, exist_ok=True)
    (pkg_dir / "__init__.py").write_text("\n")

    runner_script = Path(__file__).parent / "scripts" / "run_behave_parallel.py"
    (pkg_dir / "cli.py").write_text(runner_script.read_text(encoding="utf-8"))

    setup_path = source_dir / "setup.py"
    setup_path.write_text(
        "from setuptools import find_packages, setup\n"
        "setup(\n"
        '    name="behave-parallel",\n'
        f'    version="{BEHAVE_PARALLEL_VERSION}",\n'
        "    packages=find_packages(),\n"
        '    install_requires=["behave>=1.2.6"],\n'
        "    entry_points={\n"
        '        "console_scripts": '
        '["behave-parallel=behave_parallel.cli:main"],\n'
        "    },\n"
        ")\n"
    )

    session.install("setuptools", "wheel")
    session.install(str(source_dir))


# =============================================================================


# QUICK DEVELOPMENT SESSIONS (Fast - Run daily)
# =============================================================================


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def lint(session: nox.Session):
    """Check code formatting and linting."""
    session.install("ruff>=0.15,<0.16")
    session.run("ruff", "check", "src/", "scripts/", "examples/", "features/", "robot/")


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def format(session: nox.Session):
    """Format code with ruff. Pass --check to verify without modifying."""
    session.install("ruff>=0.15,<0.16")
    session.run("ruff", "format", *session.posargs, ".")


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def typecheck(session: nox.Session):
    """Check types with pyright."""
    session.install("pyright")
    session.install("-e", ".")
    session.run("pyright", stderr=sys.stdout)


@nox.session(python=SUPPORTED_PYTHONS, reuse_venv=True, venv_backend="uv")
def unit_tests(session: nox.Session):
    """Run BDD tests with Behave."""
    session.install("-e", ".[tests]")
    _install_behave_parallel(session)

    # Build a pre-migrated template DB so each scenario can copy it
    # instead of running 25 Alembic migrations from scratch.
    template_path = _create_template_db(session)
    session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path

    behave_cmd = session.bin + "/behave-parallel"
    parallel_args = _behave_parallel_args(session.posargs)

    # If a specific feature file is passed, run only that file
    if session.posargs and session.posargs[0].endswith(".feature"):
        args = [
            behave_cmd,
            "-q",
            *parallel_args,
            *session.posargs,
        ]
    else:
        args = [
            behave_cmd,
            "-q",
            *parallel_args,
            "features/",
            *session.posargs,
        ]

    session.env["PYTHONPATH"] = str(Path("src").resolve())
    # Prevent Rich from injecting ANSI escape codes into machine-readable
    # CLI output (JSON/YAML) which causes json.loads() failures on CI.
    session.env["NO_COLOR"] = "1"
    # Pre-compile features/ bytecode so forked parallel workers don't race
    # to write .pyc files simultaneously.  Overlayfs copy-up locks cause
    # open() to deadlock when N workers all compile uncached step files at
    # the same time (thundering-herd on __pycache__).
    session.run("python", "-m", "compileall", "-q", "features/")
    session.run(*args)


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def docs(session: nox.Session):
    """Build documentation with MkDocs."""
    session.install("-e", ".[docs]")
    # Suppress DeprecationWarning from the third-party `rx` package which calls
    # datetime.datetime.utcfromtimestamp() (deprecated since Python 3.12).
    session.env["PYTHONWARNINGS"] = "ignore::DeprecationWarning:rx"
    session.run("mkdocs", "build")


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def serve_docs(session: nox.Session):
    """Serve docs locally for development."""
    session.install("-e", ".[docs]")
    # Suppress DeprecationWarning from the third-party `rx` package which calls
    # datetime.datetime.utcfromtimestamp() (deprecated since Python 3.12).
    session.env["PYTHONWARNINGS"] = "ignore::DeprecationWarning:rx"
    session.run("mkdocs", "serve", "--dev-addr", "0.0.0.0:8000")


@nox.session(python=SUPPORTED_PYTHONS, reuse_venv=True, venv_backend="uv")
def build(session: nox.Session):
    """Build the wheel distribution."""
    session.install("build", "pip")
    session.run("python", "-m", "build", "--wheel")


@nox.session(python=SUPPORTED_PYTHONS, reuse_venv=True, venv_backend="uv")
def integration_tests(session: nox.Session):
    """Run Robot Framework integration tests (parallel via pabot).

    Defaults to conservative parallelism (<=2 processes) to avoid
    resource pressure in CI. Override via PABOT_PROCESSES or by passing
    --processes/--processes=N in session arguments.
    """
    session.install("-e", ".[tests]")
    session.env["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true"
    session.env["NO_COLOR"] = "1"
    # Override PYTHONPATH so the editable install (src/) for this project
    # is used instead of any inherited PYTHONPATH from the outer environment.
    session.env["PYTHONPATH"] = "src"

    # Propagate venv bin to PATH so Run Process in robot files finds
    # the venv's python/robot rather than the system copies.
    venv_bin = os.path.join(session.virtualenv.location, "bin")
    session.env["PATH"] = venv_bin + os.pathsep + os.environ.get("PATH", "")

    # Ensure output directory exists (CI starts with a clean checkout)
    os.makedirs("build/reports/robot", exist_ok=True)

    # Pass the venv Python path explicitly so Run Process calls use it
    # instead of relying on PATH (which may not propagate to subprocesses).
    venv_python = os.path.join(venv_bin, "python")

    # Pre-compile bytecode so that parallel pabot workers (and the
    # Python sub-processes they spawn via ``Run Python Script``) can
    # read cached .pyc files instead of each cold-compiling every
    # module from source simultaneously — avoids a thundering-herd
    # race on CI runners with high core counts.
    session.run("python", "-m", "compileall", "-q", "src/")

    # Build a pre-migrated template DB so helper scripts that call
    # setup_workspace() can copy it instead of running 25+ Alembic
    # migrations per test — critical for parallel pabot execution.
    template_path = _create_template_db(session)
    session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path

    pabot_args, robot_args = _split_pabot_args(session.posargs)
    parallel_args = _pabot_parallel_args(pabot_args)

    # TDD expected-fail listener — inverts results for @tdd_expected_fail
    # tagged tests and validates TDD tag combinations.
    # See CONTRIBUTING.md > TDD Issue Test Tags.
    # Resolved relative to this file (not CWD) so ``nox`` invocations from
    # a non-root directory still find the listener module.
    tdd_listener = str(
        Path(__file__).parent / "robot" / "tdd_expected_fail_listener.py"
    )

    session.run(
        "pabot",
        *parallel_args,
        *pabot_args,
        "--outputdir",
        "build/reports/robot",
        "--loglevel",
        "INFO",
        "--report",
        "report.html",
        "--log",
        "log.html",
        "--xunit",
        "xunit.xml",
        "--variable",
        f"PYTHON:{venv_python}",
        "--listener",
        tdd_listener,
        "--exclude",
        "slow",
        "--exclude",
        "discovery",
        "--exclude",
        "code_blocks",
        "--exclude",
        "wip",
        "--exclude",
        "E2E",
        "--exclude",
        "tdd_fixture",
        *robot_args,
        "robot/",
    )


@nox.session(python=SUPPORTED_PYTHONS, reuse_venv=True, venv_backend="uv")
def slow_integration_tests(session: nox.Session):
    """Run Robot Framework slow integration tests (parallel via pabot).

    Runs all tests tagged ``slow`` that are excluded from the standard
    ``integration_tests`` session.  Defaults to conservative parallelism
    (<=2 processes) to avoid resource pressure in CI.  Override via
    TEST_PROCESSES or by passing --processes/--processes=N in session
    arguments.
    """
    session.install("-e", ".[tests]")
    session.env["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true"
    session.env["NO_COLOR"] = "1"
    # Override PYTHONPATH so the editable install (src/) for this project
    # is used instead of any inherited PYTHONPATH from the outer environment.
    session.env["PYTHONPATH"] = "src"

    # Propagate venv bin to PATH so Run Process in robot files finds
    # the venv's python/robot rather than the system copies.
    venv_bin = os.path.join(session.virtualenv.location, "bin")
    session.env["PATH"] = venv_bin + os.pathsep + os.environ.get("PATH", "")

    # Ensure output directory exists (CI starts with a clean checkout)
    os.makedirs("build/reports/robot", exist_ok=True)

    # Pass the venv Python path explicitly so Run Process calls use it
    # instead of relying on PATH (which may not propagate to subprocesses).
    venv_python = os.path.join(venv_bin, "python")

    # Pre-compile bytecode so that parallel pabot workers (and the
    # Python sub-processes they spawn via ``Run Python Script``) can
    # read cached .pyc files instead of each cold-compiling every
    # module from source simultaneously — avoids a thundering-herd
    # race on CI runners with high core counts.
    session.run("python", "-m", "compileall", "-q", "src/")

    # Build a pre-migrated template DB so helper scripts that call
    # setup_workspace() can copy it instead of running 25+ Alembic
    # migrations per test — critical for parallel pabot execution.
    template_path = _create_template_db(session)
    session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path

    pabot_args, robot_args = _split_pabot_args(session.posargs)
    parallel_args = _pabot_parallel_args(pabot_args)

    # TDD expected-fail listener — inverts results for @tdd_expected_fail
    # tagged tests and validates TDD tag combinations.
    # See CONTRIBUTING.md > TDD Issue Test Tags.
    # Resolved relative to this file (not CWD) so ``nox`` invocations from
    # a non-root directory still find the listener module.
    tdd_listener = str(
        Path(__file__).parent / "robot" / "tdd_expected_fail_listener.py"
    )

    session.run(
        "pabot",
        *parallel_args,
        *pabot_args,
        "--outputdir",
        "build/reports/robot",
        "--loglevel",
        "INFO",
        "--report",
        "report.html",
        "--log",
        "log.html",
        "--xunit",
        "xunit.xml",
        "--variable",
        f"PYTHON:{venv_python}",
        "--listener",
        tdd_listener,
        "--include",
        "slow",
        "--exclude",
        "discovery",
        "--exclude",
        "code_blocks",
        "--exclude",
        "wip",
        "--exclude",
        "E2E",
        "--exclude",
        "tdd_fixture",
        *robot_args,
        "robot/",
    )


@nox.session(python=SUPPORTED_PYTHONS, reuse_venv=True, venv_backend="uv")
def e2e_tests(session: nox.Session):
    """Run end-to-end Robot Framework tests with real LLM API keys.

    E2E tests use zero mocking — they exercise the real CleverAgents CLI
    against real LLM API keys (Anthropic/OpenAI).  Tests are tagged with
    ``E2E`` and live in the ``robot/e2e/`` directory.

    This session is NOT included in the default ``nox`` run because it
    requires real API keys.  Run explicitly via ``nox -s e2e_tests``.

    Tests that require LLM API keys will skip gracefully when the keys
    are not present in the environment.

    Parallelism is controlled by the ``TEST_PROCESSES`` environment variable
    (default: min(cpu_count, 2)).  Override via ``TEST_PROCESSES=N nox -s
    e2e_tests`` or by passing ``--processes N`` as a session argument.
    """
    session.install("-e", ".[tests]")
    session.env["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true"
    session.env["NO_COLOR"] = "1"
    session.env["PYTHONPATH"] = "src"

    # Propagate venv bin to PATH so Run Process in robot files finds
    # the venv's python/robot rather than the system copies.
    venv_bin = os.path.join(session.virtualenv.location, "bin")
    session.env["PATH"] = venv_bin + os.pathsep + os.environ.get("PATH", "")
    venv_python = os.path.join(venv_bin, "python")

    # Ensure output directory exists
    os.makedirs("build/reports/robot-e2e", exist_ok=True)

    # Pre-compile bytecode so that parallel pabot workers (and the
    # Python sub-processes they spawn via ``Run Process``) can read
    # cached .pyc files instead of each cold-compiling every module
    # from source simultaneously — avoids a thundering-herd race on
    # CI runners with high core counts.
    session.run("python", "-m", "compileall", "-q", "src/")

    # Build a pre-migrated template DB so helper scripts that call
    # setup_workspace() can copy it instead of running 25+ Alembic
    # migrations per test — reduces per-test setup from ~1-3 s to ~1 ms.
    # Critical for parallel pabot execution where many suites start
    # simultaneously and would otherwise all race to run migrations.
    template_path = _create_template_db(session)
    session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path

    # Propagate LLM API keys from the environment into the session
    # so that real E2E tests can authenticate with providers.
    for key in (
        "ANTHROPIC_API_KEY",
        "OPENAI_API_KEY",
        "GOOGLE_API_KEY",
        "GEMINI_API_KEY",
    ):
        value = os.environ.get(key)
        if value:
            session.env[key] = value

    # Split posargs into pabot-specific args (--processes) and robot args.
    pabot_args, robot_args = _split_pabot_args(session.posargs)
    parallel_args = _pabot_parallel_args(pabot_args)

    # TDD expected-fail listener — inverts results for @tdd_expected_fail
    # tagged tests and validates TDD tag combinations.
    # See CONTRIBUTING.md > TDD Issue Test Tags.
    tdd_listener = str(
        Path(__file__).parent / "robot" / "tdd_expected_fail_listener.py"
    )

    # Use pabot (parallel Robot Framework runner) instead of the sequential
    # ``robot`` runner.  Each E2E suite file runs in its own pabot worker
    # process, so independent suites execute concurrently rather than
    # serially.  Suite-level isolation (separate CLEVERAGENTS_HOME per
    # suite) ensures workers do not share database state.
    session.run(
        "pabot",
        *parallel_args,
        *pabot_args,
        "--outputdir",
        "build/reports/robot-e2e",
        "--loglevel",
        "INFO",
        "--report",
        "report.html",
        "--log",
        "log.html",
        "--xunit",
        "xunit.xml",
        "--variable",
        f"PYTHON:{venv_python}",
        "--include",
        "E2E",
        "--listener",
        tdd_listener,
        *robot_args,
        "robot/e2e/",
    )


COVERAGE_THRESHOLD = 97  # Temporarily lowered due to many @tdd_expected_fail tests
# see issues #4183 and #4184


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def coverage_report(session: nox.Session):
    """Generate coverage report from Behave tests.

    Runs all behave features in a single process under slipcover.
    The in-process behave-parallel runner avoids subprocess overhead,
    so a single slipcover invocation collects coverage for the entire
    suite -- no per-worker files or merge step required.

    Coverage threshold is enforced at >=97%.

    On success, emits: COVERAGE OK: <pct>% (threshold: 97%)
    On failure, emits: COVERAGE FAILED: <pct>% < 97% threshold
    Both are single-line, CI-parseable summary strings.
    """
    session.install("-e", ".[tests]")
    _install_behave_parallel(session)

    os.makedirs("build", exist_ok=True)

    # Build a pre-migrated template DB so each scenario can copy it
    # instead of running 25 Alembic migrations from scratch.
    template_path = _create_template_db(session)
    session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path

    session.env["PYTHONPATH"] = str(Path("src").resolve())
    session.env["NO_COLOR"] = "1"
    source_paths = "src"
    omit_patterns = ",".join(
        [
            "*/tests/*",
            "*/test_*",
            "features/*",
            "*/features/*",
            "*/__pycache__/*",
            "*/site-packages/*",
            "*/dependency_injector/*",
            "*/venv/*",
            "*/.venv/*",
            "*/.nox/*",
            "src/cleveragents/discovery/*",
        ]
    )

    # Force sequential mode so slipcover wraps the entire process.
    session.env["BEHAVE_PARALLEL_COVERAGE"] = "1"

    # Clean up any existing slipcover data
    for path in Path("build").glob(".slipcover.*.json"):
        path.unlink()

    # Build behave-parallel args (sequential for coverage).
    behave_cmd = session.bin + "/behave-parallel"
    if session.posargs and session.posargs[0].endswith(".feature"):
        behave_args = [
            behave_cmd,
            "-q",
            "--no-capture",
            *session.posargs,
        ]
    else:
        behave_args = [
            behave_cmd,
            "-q",
            "--no-capture",
            "features/",
            *session.posargs,
        ]

    # Wrap the entire behave-parallel run under slipcover.
    # A single process produces a single JSON output file directly.
    # Allow exit code 1 (test failures) — coverage data is still produced.
    session.run(
        "python",
        "-m",
        "slipcover",
        "--json",
        "--out",
        "build/coverage.json",
        "--source",
        source_paths,
        "--omit",
        omit_patterns,
        "--",
        *behave_args,
        success_codes=[0, 1],
    )

    # Generate XML report for CI
    session.run(
        "python",
        "-m",
        "slipcover",
        "--merge",
        "build/coverage.json",
        "--xml",
        "--out",
        "build/coverage.xml",
    )

    # Generate terminal report and check threshold.
    # slipcover exits with code 2 if below threshold; nox intercepts the exit.
    report_path = "build/coverage-report.txt"
    session.run(
        "python",
        "-m",
        "slipcover",
        "--merge",
        "build/coverage.json",
        "--out",
        report_path,
        "--source",
        source_paths,
        "--omit",
        omit_patterns,
        f"--fail-under={COVERAGE_THRESHOLD}",
        success_codes=[0, 2],
        silent=False,
    )

    try:
        with open(report_path) as report_file:
            print(report_file.read(), end="")
    except FileNotFoundError:
        session.log(f"Slipcover report not found at {report_path}")

    # Parse the total percentage from the slipcover JSON report for the summary
    total_pct: float = 0.0
    try:
        with open("build/coverage.json") as f:
            payload = json.load(f)

        summary = payload.get("summary") or payload.get("totals") or {}
        percent_value = None
        for key in (
            "percent_covered",
            "percent_covered_display",
            "line_coverage_percent",
        ):
            percent_value = summary.get(key)
            if percent_value is not None:
                break

        if percent_value is None:
            covered = (
                summary.get("lines_covered")
                or summary.get("covered_lines")
                or summary.get("covered")
            )
            total = (
                summary.get("lines")
                or summary.get("total_lines")
                or summary.get("num_lines")
                or summary.get("total")
            )
            if covered is not None and total:
                percent_value = (float(covered) / float(total)) * 100.0

        if isinstance(percent_value, str):
            percent_value = percent_value.strip().rstrip("%")

        if percent_value is not None:
            total_pct = float(percent_value)
    except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError):
        total_pct = 0.0

    rounded_pct = round(total_pct, 1)

    if rounded_pct >= COVERAGE_THRESHOLD:
        session.log(f"COVERAGE OK: {rounded_pct}% (threshold: {COVERAGE_THRESHOLD}%)")
    else:
        session.error(
            f"COVERAGE FAILED: {rounded_pct}% < {COVERAGE_THRESHOLD}% threshold"
        )


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def pre_commit(session: nox.Session):
    """Run all pre-commit hooks on all files."""
    session.install("-e", ".[dev]")
    session.run("pre-commit", "run", "--all-files", *session.posargs)


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def security_scan(session: nox.Session):
    """Run security checks matching Forgejo CI: bandit + semgrep + vulture.

    Steps:
      1. Bandit medium-severity report (non-blocking JSON export to build/)
      2. Bandit high-severity gate (blocks on findings)
      3. Semgrep custom rules from .semgrep.yml (blocks on ERROR-level findings)
      4. Vulture dead-code detection (blocks on confidence >=80%)

    Severity gates:
      - Bandit HIGH: hard fail (blocks merge)
      - Bandit MEDIUM: report-only (review, do not block)
      - Semgrep ERROR: hard fail (eval/exec/os.system detection)
      - Semgrep WARNING: report-only (pickle.loads)
      - Vulture >=80% confidence: hard fail
    """
    session.install("-e", ".[dev]")
    # Python 3.13 no longer bundles pkg_resources. Semgrep transitively imports
    # it via opentelemetry instrumentation, so pin setuptools to a version that
    # still provides pkg_resources.
    session.install("setuptools<81")

    # Ensure output directory exists (CI starts with a clean checkout)
    os.makedirs("build", exist_ok=True)

    # Step 1: Bandit - medium-severity report (non-blocking, mirrors CI JSON export)
    session.run(
        "bandit",
        "-c",
        "pyproject.toml",
        "-r",
        "src/cleveragents",
        "--severity-level",
        "medium",
        "--format",
        "json",
        "--output",
        "build/bandit-report.json",
        success_codes=[0, 1],
    )

    # Step 2: Bandit - high-severity gate (blocks on findings)
    session.run(
        "bandit",
        "-c",
        "pyproject.toml",
        "-r",
        "src/cleveragents",
        "--severity-level",
        "high",
    )

    # Step 3: Semgrep custom rules (blocks on ERROR-severity matches)
    semgrep_config = Path(".semgrep.yml")
    if semgrep_config.exists():
        session.run(
            "semgrep",
            "--config=.semgrep.yml",
            "--error",
            "--quiet",
            "src/",
            success_codes=[0, 1],
        )
    else:
        session.warn("No .semgrep.yml found, skipping semgrep scan")

    # Step 4: Vulture dead-code detection
    session.run(
        "vulture",
        "src/cleveragents",
        "vulture_whitelist.py",
        "--min-confidence",
        "80",
        "--exclude",
        "src/cleveragents/discovery",
    )


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def dead_code(session: nox.Session):
    """Run vulture to detect dead code."""
    session.install("-e", ".[dev]")
    session.run(
        "vulture",
        "src/cleveragents",
        "vulture_whitelist.py",
        "--min-confidence",
        "80",
        "--exclude",
        "src/cleveragents/discovery",
    )


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def complexity(session: nox.Session):
    """Check code complexity using radon."""
    session.install("-e", ".[dev]")
    session.run(
        "radon",
        "cc",
        "src/cleveragents",
        "--min",
        "C",
        "--show-complexity",
        "--total-average",
    )


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def adr_compliance(session: nox.Session):
    """Check code compliance with Architecture Decision Records."""
    session.install("-e", ".[dev]")
    session.run("python", "scripts/check-adr-compliance.py")


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def benchmark(session: nox.Session):
    """Run Airspeed Velocity benchmarks and publish results."""
    session.install("-e", ".[tests]")
    config_path = "asv.conf.json"
    session.run(
        "asv",
        "machine",
        "--machine=forgejo-runner",
        "--os=Linux_6.x",
        "--arch=x86_64",
        "--num_cpu=32",
        "--ram=32GB",
        "--cpu=AMD",
        f"--config={config_path}",
    )
    session.run(
        "asv",
        "run",
        "--machine=forgejo-runner",
        "--append-samples",
        "--launch-method=spawn",
        "--show-stderr",
        "--verbose",
        f"--config={config_path}",
        success_codes=[0, 2],
    )
    session.run("asv", "publish", f"--config={config_path}")


@nox.session(python=DEFAULT_PYTHON, reuse_venv=True, venv_backend="uv")
def benchmark_regression(session: nox.Session):
    """Run Airspeed Velocity benchmarks regression test."""
    session.install("-e", ".[tests]")
    config_path = "asv.conf.json"
    asv_base_sha = os.environ.get("ASV_BASE_SHA", "master")
    session.run(
        "asv",
        "machine",
        "--machine=forgejo-runner",
        "--os=Linux_6.x",
        "--arch=x86_64",
        "--num_cpu=32",
        "--ram=32GB",
        "--cpu=AMD",
        f"--config={config_path}",
    )
    session.run(
        "asv",
        "continuous",
        "--machine=forgejo-runner",
        "--append-samples",
        "--show-stderr",
        "--verbose",
        "--factor=1.50",
        f"--config={config_path}",
        asv_base_sha,
        "HEAD",
        success_codes=[0, 2],
    )
    session.run("asv", "publish", f"--config={config_path}")


# Sessions to run by default when running `nox` without arguments
nox.options.sessions = [
    "lint",  # ~5-10 seconds
    "format",  # ~5-10 seconds (auto-fixes issues)
    "typecheck",  # ~10-20 seconds
    "security_scan",  # bandit + vulture (mirrors Forgejo CI security job)
    "dead_code",  # vulture dead-code detection
    "unit_tests",  # ~30-60 seconds (without ML deps)
    "integration_tests",
    "docs",  # ~10-30 seconds
    "build",  # ~5-10 seconds
    "benchmark",  # ASV benchmarks for performance tracking
    "coverage_report",  # ~30-60 seconds (without ML deps)
]
