"""CLI tests for `attractor prune` and the `--no-auto-prune` flag.

Engine-side prune coverage lives in `tests/test_engine/test_prune.py`;
this file drives the CLI surface end-to-end via `Click.CliRunner`.
"""

from __future__ import annotations

import json
import os
import re
from datetime import UTC, datetime
from pathlib import Path

from click.testing import CliRunner

from attractor.checkpoint import Author, BranchStore
from attractor.cli.main import cli
from attractor.engine.journal import event_path

_TOOL_ONLY = """\
digraph T {
    graph [ default_max_visits = 2 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    build [shape=parallelogram, script="echo built > build.log"]
    start -> build -> exit
}
"""

_HUMAN_GATE = """\
digraph HG {
    graph [ default_max_visits = 3 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    prep  [shape=parallelogram, script="echo prep"]
    decide [shape=hexagon, label="Approve?"]
    start  -> prep -> decide
    decide -> exit  [label="approve"]
    decide -> prep  [label="revise"]
}
"""


def _run_workflow(seeded_repo: Path, source: str) -> str:
    """Helper: drop a workflow, run it, return the resulting run-id.

    Returns the first 8 chars of the run-id by parsing the CLI banner;
    sufficient for `attractor run show <prefix>` style probes if we ever
    need them, though the current tests use `Engine.list_all_run_ids`.
    """
    wf = seeded_repo / "wf.dot"
    wf.write_text(source, encoding="utf-8")
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code in (0, 1), result.output
    finally:
        os.chdir(cwd)
    return result.output


def _list_runs(seeded_repo: Path) -> list[str]:
    from attractor.engine import Engine
    return Engine(seeded_repo).list_all_run_ids()


def _write_old_schema_corrupt_run(seeded_repo: Path, run_id: str) -> None:
    import pygit2

    repo = pygit2.Repository(str(seeded_repo))
    store = BranchStore(repo, Author())
    ref = f"refs/heads/attractor/run/{run_id}/state"
    now = datetime.now(UTC).isoformat()
    store.write_entry(
        ref,
        event_path(0),
        json.dumps(
            {
                "seq": 0,
                "run_id": run_id,
                "timestamp": now,
                "kind": "RunInitialized",
                "workflow_dot": "{}",
                "workflow_hash": "old-schema-test",
                "base_ref": "HEAD",
            }
        ).encode("utf-8"),
        "checkpoint: test corrupt run init",
    )
    store.write_entry(
        ref,
        event_path(1),
        json.dumps(
            {
                "seq": 1,
                "run_id": run_id,
                "timestamp": now,
                "kind": "NodeCompleted",
                "node_id": "legacy",
                "visit": 1,
                # Old partial journals had success but not the newer status field.
                "success": True,
                "captured_output": "",
                "duration_ms": 0,
                "next_node": None,
                "worktree_commit_after": None,
            }
        ).encode("utf-8"),
        "checkpoint: test corrupt run legacy node",
    )


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_no_selectors_errors_with_hint(seeded_repo: Path) -> None:
    """Empty selector → exit 2, error mentions which flags help."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(cli, ["prune"])
        assert result.exit_code == 2, result.output
        assert "selector" in result.output.lower()
        assert "--all-completed" in result.output or "--dry-run" in result.output
    finally:
        os.chdir(cwd)


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_all_completed_removes_completed_runs(
    seeded_repo: Path,
) -> None:
    """Run a workflow to COMPLETED, then `prune --all-completed`."""
    _run_workflow(seeded_repo, _TOOL_ONLY)
    assert len(_list_runs(seeded_repo)) == 1

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(cli, ["prune", "--all-completed"])
        assert result.exit_code == 0, result.output
        assert "pruned" in result.output.lower()
    finally:
        os.chdir(cwd)
    assert _list_runs(seeded_repo) == []


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_dry_run_does_not_remove(seeded_repo: Path) -> None:
    _run_workflow(seeded_repo, _TOOL_ONLY)
    before = _list_runs(seeded_repo)
    assert len(before) == 1

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(cli, ["prune", "--all-completed", "--dry-run"])
        assert result.exit_code == 0, result.output
        assert "would prune" in result.output.lower()
    finally:
        os.chdir(cwd)
    assert _list_runs(seeded_repo) == before


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_no_match_returns_exit_1(seeded_repo: Path) -> None:
    """Selectors matched zero runs → exit 1 (distinct from "did the work")."""
    _run_workflow(seeded_repo, _TOOL_ONLY)  # one COMPLETED run.

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        # Filter for paused → no matches.
        result = CliRunner().invoke(cli, ["prune", "--status", "paused"])
        assert result.exit_code == 1, result.output
        assert "nothing to prune" in result.output.lower()
    finally:
        os.chdir(cwd)


# [int->REQ-CLI-PRUNE-SAFETY]
def test_prune_skips_old_schema_corrupt_journal(seeded_repo: Path) -> None:
    """Old run journals should not crash prune selection.

    SPEC §14.1 says missing/corrupt state branches are warned and skipped;
    this pins the old pre-OutcomeStatus NodeCompleted shape observed in
    local dogfood refs.
    """
    _write_old_schema_corrupt_run(seeded_repo, "legacy-run")

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(cli, ["prune", "--older-than", "0s", "--dry-run"])
        assert result.exit_code == 1, result.output
        assert "warn: run legacy-run" in result.output
        assert "corrupt journal entry" in result.output
        assert "nothing to prune" in result.output.lower()
    finally:
        os.chdir(cwd)


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_older_than_zero_matches_all(seeded_repo: Path) -> None:
    """`--older-than 0s --status completed` removes any completed run."""
    _run_workflow(seeded_repo, _TOOL_ONLY)
    assert len(_list_runs(seeded_repo)) == 1

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(
            cli, ["prune", "--status", "completed", "--older-than", "0s"]
        )
        assert result.exit_code == 0, result.output
    finally:
        os.chdir(cwd)
    assert _list_runs(seeded_repo) == []


# [int->REQ-CLI-PRUNE-SELECTORS]
def test_prune_invalid_duration_exit_2(seeded_repo: Path) -> None:
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(
            cli, ["prune", "--all-completed", "--older-than", "5xy"]
        )
        assert result.exit_code == 2, result.output
        assert "duration" in result.output.lower()
    finally:
        os.chdir(cwd)


# [int->REQ-CLI-PRUNE-SAFETY]
def test_prune_paused_refused_without_force(seeded_repo: Path) -> None:
    """A paused run shouldn't be pruned by an unguarded selector."""
    _run_workflow(seeded_repo, _HUMAN_GATE)  # → PAUSED
    run_ids = _list_runs(seeded_repo)
    assert len(run_ids) == 1

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        # Selecting by run-id forces consideration; refusal still fires.
        result = CliRunner().invoke(cli, ["prune", "--run-id", run_ids[0]])
        assert result.exit_code == 2, result.output
        assert "refused" in result.output.lower() or "paused" in result.output.lower()
    finally:
        os.chdir(cwd)
    # State branch survives the refusal.
    assert _list_runs(seeded_repo) == run_ids


# [int->REQ-CLI-PRUNE-SAFETY]
def test_prune_paused_with_force_succeeds(seeded_repo: Path) -> None:
    _run_workflow(seeded_repo, _HUMAN_GATE)
    run_ids = _list_runs(seeded_repo)

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        result = CliRunner().invoke(
            cli, ["prune", "--run-id", run_ids[0], "--force"]
        )
        assert result.exit_code == 0, result.output
    finally:
        os.chdir(cwd)
    assert _list_runs(seeded_repo) == []


# [int->REQ-CLI-AUTOPRUNE]
def test_no_auto_prune_flag_skips_auto_pass(seeded_repo: Path) -> None:
    """With `--no-auto-prune`, the auto-prune banner never appears.

    We exercise the flag path by running once (creating a COMPLETED
    run that auto-prune would pick up if retention allowed) and then
    running again with --no-auto-prune. Even if retention fired, the
    flag suppresses the side effect — so no `auto-prune:` line.
    """
    wf = seeded_repo / "wf.dot"
    wf.write_text(_TOOL_ONLY, encoding="utf-8")

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        runner = CliRunner()
        # First run: nothing to auto-prune, no banner expected.
        first = runner.invoke(cli, ["run", str(wf)])
        assert first.exit_code == 0, first.output
        # Second run with --no-auto-prune: even if there were matching
        # COMPLETED runs older than retention, the flag would skip the
        # pass — and the banner is the only observable side effect.
        second = runner.invoke(cli, ["run", "--no-auto-prune", str(wf)])
        assert second.exit_code == 0, second.output
        assert not re.search(r"auto-prune:", second.output, re.IGNORECASE)
    finally:
        os.chdir(cwd)
