"""CLI tests for `attractor run`, `run resume`, `run respond`,
`run list`, `run show`.

In-process tests (Click `CliRunner`) cover the most paths. The §16
DoD acceptance test in `test_dod_acceptance.py` uses `subprocess.run`
against the built `attractor` entry point to prove end-to-end.
"""

from __future__ import annotations

import json
import os
from datetime import UTC, datetime
from pathlib import Path

import pygit2
import pytest
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"]
}
"""


# [unit->REQ-CLI-RUN-LIFECYCLE]
# [unit->REQ-LAUNCH-PATH-PREFLIGHT]
# [int->REQ-DOD-WORKFLOW-RUN]
def test_run_tool_only_completes_via_cli(seeded_repo: Path) -> None:
    """`attractor run workflow.dot` on a tool-only workflow exits 0."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_TOOL_ONLY, encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 0, result.output + (result.stderr or "")
        assert "Run-id:" in result.output
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
def test_run_resolves_project_workflow_name_without_dot(seeded_repo: Path) -> None:
    """`attractor run NAME` resolves `workflows/NAME.dot` in the cwd repo."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        workflows = seeded_repo / "workflows"
        workflows.mkdir()
        (workflows / "smoke.dot").write_text(_TOOL_ONLY, encoding="utf-8")

        runner = CliRunner()
        result = runner.invoke(cli, ["run", "smoke"])

        assert result.exit_code == 0, result.output + (result.stderr or "")
        assert "Run-id:" in result.output
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
def test_run_resolves_user_workflow_name_without_dot(
    seeded_repo: Path,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """`attractor run NAME` falls back to the user-profile workflow library."""
    user_workflows = tmp_path / "user-workflows"
    user_workflows.mkdir()
    (user_workflows / "shared.dot").write_text(_TOOL_ONLY, encoding="utf-8")
    monkeypatch.setenv("ATTRACTOR_USER_WORKFLOWS", str(user_workflows))

    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        runner = CliRunner()
        result = runner.invoke(cli, ["run", "shared"])

        assert result.exit_code == 0, result.output + (result.stderr or "")
        assert "Run-id:" in result.output
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
# [int->REQ-CLI-RUN-LIFECYCLE]
def test_run_pauses_and_lists_then_resume(seeded_repo: Path) -> None:
    """Pause at hexagon → list → respond → resume → exit."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_HUMAN_GATE, encoding="utf-8")
        runner = CliRunner()

        # 1. Run → pause.
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 0, result.output + (result.stderr or "")
        assert "paused at gate" in result.output.lower()

        # 2. List → see the paused run.
        result = runner.invoke(cli, ["run", "list"])
        assert result.exit_code == 0
        assert "paused" in result.output.lower()

        # Find the run-id from the worktree directory.
        repo = pygit2.Repository(str(seeded_repo))
        state_branches = [
            r for r in repo.references
            if r.endswith("/state") and "attractor/run/" in r
        ]
        assert state_branches
        run_id = state_branches[0].split("/")[-2]

        # 3. Respond.
        result = runner.invoke(cli, ["run", "respond", run_id, "approve"])
        assert result.exit_code == 0
        assert "response recorded" in result.output

        # 4. Resume → COMPLETED.
        result = runner.invoke(cli, ["run", "resume", run_id])
        assert result.exit_code == 0
        assert "completed" in result.output.lower()
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-INSPECT]
def test_run_list_skips_corrupt_live_worktree_journal(seeded_repo: Path) -> None:
    """A stale pre-schema journal should not crash `run list`."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_HUMAN_GATE, encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 0, result.output + (result.stderr or "")

        repo = pygit2.Repository(str(seeded_repo))
        state_ref = next(
            r for r in repo.references
            if r.endswith("/state") and "attractor/run/" in r
        )
        run_id = state_ref.split("/")[-2]
        now = datetime.now(UTC).isoformat()
        BranchStore(repo, Author()).write_entry(
            state_ref,
            event_path(1),
            json.dumps(
                {
                    "seq": 1,
                    "run_id": run_id,
                    "timestamp": now,
                    "kind": "NodeCompleted",
                    "node_id": "legacy",
                    "visit": 1,
                    "success": True,
                    "captured_output": "",
                    "duration_ms": 0,
                    "next_node": None,
                    "worktree_commit_after": None,
                }
            ).encode("utf-8"),
            "checkpoint: test corrupt live run",
        )

        result = runner.invoke(cli, ["run", "list"])
        assert result.exit_code == 0, result.output + (result.stderr or "")
        assert "traceback" not in (result.output + (result.stderr or "")).lower()
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-INSPECT]
def test_run_show_after_completion(seeded_repo: Path) -> None:
    """`attractor run show <id>` after a completed run prints summary."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_TOOL_ONLY, encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 0

        # Discover run-id.
        repo = pygit2.Repository(str(seeded_repo))
        state_branches = [
            r for r in repo.references
            if r.endswith("/state") and "attractor/run/" in r
        ]
        run_id = state_branches[0].split("/")[-2]

        result = runner.invoke(cli, ["run", "show", run_id])
        assert result.exit_code == 0
        assert run_id in result.output
        assert "completed" in result.output.lower()
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-INSPECT]
def test_run_show_unknown_run_id_exits_two(seeded_repo: Path) -> None:
    """Unknown run-id → exit 2 with a clear error."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        runner = CliRunner()
        result = runner.invoke(cli, ["run", "show", "00000000-0000-4000-8000-000000000000"])
        assert result.exit_code == 2
        assert "unknown run" in (result.stderr or "").lower()
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
def test_run_respond_unknown_choice_exits_two(seeded_repo: Path) -> None:
    """Unknown choice → exit 2; valid choices surface."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_HUMAN_GATE, encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 0

        repo = pygit2.Repository(str(seeded_repo))
        first_state_ref = next(
            r for r in repo.references
            if r.endswith("/state") and "attractor/run/" in r
        )
        run_id = first_state_ref.split("/")[-2]

        result = runner.invoke(cli, ["run", "respond", run_id, "ship-it"])
        assert result.exit_code == 2
        assert "unknown choice" in (result.stderr or "").lower()
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
def test_run_input_flag_copies_file(seeded_repo: Path) -> None:
    """`--input name=path` copies a host file into the worktree."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        # Workflow that reads `goal.md` via a tool node — exit non-zero
        # if the file isn't there so we can prove the copy happened.
        # Use the sh / pwsh common subset from SPEC §6.8.
        wf.write_text(
            """\
digraph G {
    graph [ default_max_visits = 1 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    read [shape=parallelogram, script="cat goal.md > input-copy.log", goal_gate=true]
    start -> read -> exit
}
""",
            encoding="utf-8",
        )
        goal = seeded_repo / "goal.md"
        goal.write_text("ship it\n", encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(
            cli, ["run", str(wf), "--input", f"goal.md={goal}"]
        )
        assert result.exit_code == 0, result.output + (result.stderr or "")
    finally:
        os.chdir(cwd)


# [unit->REQ-EXEC-RUN-ARGUMENTS]
def test_run_workflow_arguments_are_materialized(seeded_repo: Path) -> None:
    """Extra positionals bind to graph arguments and land in JSON."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(
            """\
digraph G {
    graph [
        default_max_visits = 1
        arguments = "
            issue_url { type: url; required: true; help: GitHub issue URL; }
        "
    ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    read [
        shape=parallelogram,
        goal_gate=true,
        script="cat attractor-args.json"
    ]
    start -> read -> exit
}
""",
            encoding="utf-8",
        )
        runner = CliRunner()
        result = runner.invoke(
            cli,
            [
                "run",
                str(wf),
                "https://github.com/owner/repo/issues/123",
            ],
        )
        assert result.exit_code == 0, result.output + (result.stderr or "")
    finally:
        os.chdir(cwd)


# [unit->REQ-EXEC-RUN-ARGUMENTS]
def test_run_workflow_arguments_missing_required_exits_two(
    seeded_repo: Path,
) -> None:
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(
            """\
digraph G {
    graph [ arguments = "issue_url { type: url; }" ]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    start -> exit
}
""",
            encoding="utf-8",
        )
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 2
        assert "missing required run argument" in (result.stderr or "")
    finally:
        os.chdir(cwd)


# [unit->REQ-EXEC-RUN-ARGUMENTS]
def test_run_extra_arg_without_declaration_exits_two(
    seeded_repo: Path,
) -> None:
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(_TOOL_ONLY, encoding="utf-8")
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf), "unexpected"])
        assert result.exit_code == 2
        assert "does not declare arguments" in (result.stderr or "")
    finally:
        os.chdir(cwd)


# [unit->REQ-CLI-RUN-LIFECYCLE]
def test_run_validate_failure_does_not_create_worktree(seeded_repo: Path) -> None:
    """An invalid workflow exits 2 BEFORE any worktree is created."""
    cwd = os.getcwd()
    try:
        os.chdir(seeded_repo)
        wf = seeded_repo / "wf.dot"
        wf.write_text(
            """\
digraph Z {
    a [shape=Mdiamond, label="A"]
    a -> a
}
""",
            encoding="utf-8",
        )
        runner = CliRunner()
        result = runner.invoke(cli, ["run", str(wf)])
        assert result.exit_code == 2
        # No worktree directory created — the CLI rejects invalid
        # workflows BEFORE the engine opens any state.
        worktree_root = seeded_repo / ".attractor" / "worktrees"
        if worktree_root.exists():
            assert list(worktree_root.iterdir()) == []
    finally:
        os.chdir(cwd)
