"""§16 v0.1 Definition-of-Done acceptance test.

> A user can write a 5-node DOT workflow, run it end-to-end on a fresh
> repo, hit a human gate, resume it, and reach `exit`.

This test drives the `attractor` CLI as a subprocess against a fresh
temp git repo with a 5-node workflow. The flow:

    1. `attractor validate <wf.dot>` → exit 0
    2. `attractor init` → exit 0
    3. `attractor run <wf.dot>` → exit 0, status=PAUSED at the gate
    4. `attractor run list` → shows the paused run
    5. `attractor run show <id>` → shows the gate's prompt + choices
    6. `attractor run respond <id> approve` → exit 0
    7. `attractor run resume <id>` → exit 0, status=COMPLETED
    8. `attractor run show <id>` → reports `completed`
    9. All run state lives under `refs/heads/attractor/` (REQ-DOD-LOCAL-FIRST)

Uses `uv run attractor` so we're testing the entry point that ships,
not the import-level wiring.
"""

from __future__ import annotations

import os
import shutil
import subprocess
from pathlib import Path

import pygit2
import pytest

from attractor.testing import create_test_repo

# A 5-node workflow exercising the §16 acceptance:
# start → prep (tool) → decide (human) → finalise (tool) → exit.
# Five nodes; one of them is a human gate per the DoD bullet.
# `echo` works uniformly under both `sh -c` and `pwsh -NoProfile -Command`
# so this fixture is OS-independent.
_DOD_WORKFLOW = """\
digraph DoD {
    graph [
        goal = "v0.1 acceptance: tool work, human gate, tool finalise."
        default_max_visits = 3
    ]
    start    [shape=Mdiamond, label="Start"]
    exit     [shape=Msquare,  label="Exit"]
    prep     [shape=parallelogram, label="Prep",
              script="echo prepped > prep.log"]
    decide   [shape=hexagon, label="Approve?",
              prompt="Inspect prep.log and approve or revise."]
    finalise [shape=parallelogram, label="Finalise",
              script="echo final > final.log"]
    start    -> prep -> decide
    decide   -> finalise [label="approve"]
    decide   -> prep     [label="revise"]
    finalise -> exit
}
"""


def _has_uv() -> bool:
    return shutil.which("uv") is not None


def _has_production_shell() -> bool:
    """True iff the production shell pair for this host is on PATH.

    SPEC §6.8 documents `sh -c` on Unix and `pwsh -NoProfile -Command`
    on Windows. The in-process CLI tests monkeypatch the shell resolver
    to fall back to whatever shell the host has; the SUBPROCESS-based
    DoD test cannot do that (the child Python doesn't inherit the
    monkeypatch). When the production shell is missing — typically
    `pwsh` on Windows hosts that only have Windows PowerShell 5 — the
    subprocess test skips. The in-process `test_dod_in_process` below
    still runs and proves the §16 acceptance via the same code path.
    """
    import sys as _sys

    if _sys.platform == "win32":
        return shutil.which("pwsh") is not None
    return shutil.which("sh") is not None


def _attractor_via_uv(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
    """Run `uv run --project <worktree-root> attractor <args>` with cwd=repo.

    `--project` pins UV to our worktree's pyproject so the version of
    attractor under test is the one we just built — not whatever the
    user might happen to have installed.
    """
    worktree_root = Path(__file__).resolve().parents[2]
    return subprocess.run(
        [
            "uv",
            "run",
            "--project",
            str(worktree_root),
            "attractor",
            *args,
        ],
        cwd=str(repo),
        capture_output=True,
        text=True,
    )


@pytest.fixture()
def dod_repo(tmp_path: Path) -> Path:
    """A bare-bones git repo for the §16 acceptance test."""
    return create_test_repo(tmp_path / "dod-repo")


@pytest.mark.skipif(not _has_uv(), reason="uv not on PATH")
@pytest.mark.skipif(
    not _has_production_shell(),
    reason=(
        "production shell (pwsh on Windows / sh on Unix) not on PATH; "
        "in-process DoD test below covers the same scenario"
    ),
)
# [int->REQ-DOD-WORKFLOW-RUN]
# [int->REQ-DOD-LOCAL-FIRST]
# [int->REQ-CLI-RUN-LIFECYCLE]
# [int->REQ-CLI-INSPECT]
# [int->REQ-CLI-SCAFFOLD]
# [int->REQ-HUMAN-GATE]
def test_dod_v01_acceptance_via_cli_subprocess(dod_repo: Path) -> None:
    """Run the v0.1 acceptance scenario end-to-end through `attractor` CLI."""
    wf = dod_repo / "wf.dot"
    wf.write_text(_DOD_WORKFLOW, encoding="utf-8")

    # 1. validate.
    r = _attractor_via_uv(dod_repo, "validate", str(wf))
    assert r.returncode == 0, (
        f"validate failed: {r.stdout}\n{r.stderr}"
    )
    assert "valid:" in r.stdout

    # 2. init.
    r = _attractor_via_uv(dod_repo, "init")
    assert r.returncode == 0, f"init failed: {r.stdout}\n{r.stderr}"

    # 3. run → pause at the human gate.
    r = _attractor_via_uv(dod_repo, "run", str(wf))
    assert r.returncode == 0, (
        f"run failed:\nstdout: {r.stdout}\nstderr: {r.stderr}"
    )
    assert "paused" in (r.stdout + r.stderr).lower()

    # 4. list shows the paused run.
    r = _attractor_via_uv(dod_repo, "run", "list")
    assert r.returncode == 0
    assert "paused" in r.stdout.lower()

    # 5. discover the run-id from the state branch ref.
    repo = pygit2.Repository(str(dod_repo))
    state_branches = [
        ref for ref in repo.references
        if ref.endswith("/state") and "attractor/run/" in ref
    ]
    assert state_branches, "no attractor state branch present"
    run_id = state_branches[0].split("/")[-2]

    # 6. show prints the gate's prompt.
    r = _attractor_via_uv(dod_repo, "run", "show", run_id)
    assert r.returncode == 0
    assert run_id in r.stdout
    # The Rich table writes ANSI; just spot-check the prompt text.
    assert "approve" in r.stdout.lower()

    # 7. respond approve.
    r = _attractor_via_uv(dod_repo, "run", "respond", run_id, "approve")
    assert r.returncode == 0, f"respond failed: {r.stdout}\n{r.stderr}"

    # 8. resume → COMPLETED.
    r = _attractor_via_uv(dod_repo, "run", "resume", run_id)
    assert r.returncode == 0, f"resume failed: {r.stdout}\n{r.stderr}"
    assert "completed" in (r.stdout + r.stderr).lower()

    # 9. show after completion.
    r = _attractor_via_uv(dod_repo, "run", "show", run_id)
    assert r.returncode == 0
    assert "completed" in r.stdout.lower()

    # 10. (REQ-DOD-LOCAL-FIRST) All run state lives under
    # `refs/heads/attractor/`. No other Attractor-namespaced refs.
    for ref in repo.references:
        if "attractor" in ref:
            assert ref.startswith("refs/heads/attractor/"), (
                f"unexpected attractor ref {ref} — must start with "
                "refs/heads/attractor/"
            )

    # 11. (REQ-DOD-LOCAL-FIRST) No remote was configured during the run.
    remotes = list(repo.remotes)
    for remote in remotes:
        remote_url = remote.url or ""
        assert remote_url.startswith(("file://", str(dod_repo))), (
            f"run created a non-local remote {remote_url!r} — "
            "violates §1 local-first"
        )

    # 12. Worktree branch persists for review.
    worktree_branches = [
        ref for ref in repo.references
        if ref.endswith("/worktree") and "attractor/run/" in ref
    ]
    assert worktree_branches, (
        "worktree branch should persist for `git diff main..<branch>`"
    )

    # 13. State journal records the gate response.
    state_ref = state_branches[0]
    state_log = subprocess.run(
        ["git", "-C", str(dod_repo), "log", "--format=%s", state_ref],
        capture_output=True,
        text=True,
        check=True,
    )
    assert "GateResponded" in state_log.stdout
    assert "RunFinalized" in state_log.stdout


@pytest.mark.skipif(not _has_uv(), reason="uv not on PATH")
# [int->REQ-CLI-SCAFFOLD]
# [int->REQ-CLI-RUN-LIFECYCLE]
# [int->REQ-CLI-INSPECT]
def test_dod_attractor_help_advertises_seven_subcommands(dod_repo: Path) -> None:
    """`attractor --help` documents init / validate / run; `run --help`
    documents resume / respond / list / show. SPEC §13 surface check."""
    r = _attractor_via_uv(dod_repo, "--help")
    assert r.returncode == 0
    assert "init" in r.stdout
    assert "validate" in r.stdout
    assert "run" in r.stdout
    r = _attractor_via_uv(dod_repo, "run", "--help")
    assert r.returncode == 0
    assert "list" in r.stdout
    assert "respond" in r.stdout
    assert "resume" in r.stdout
    assert "show" in r.stdout


# [int->REQ-DOD-WORKFLOW-RUN]
# [int->REQ-DOD-LOCAL-FIRST]
# [int->REQ-CLI-RUN-LIFECYCLE]
# [int->REQ-CLI-INSPECT]
# [int->REQ-CLI-SCAFFOLD]
# [int->REQ-HUMAN-GATE]
def test_dod_v01_acceptance_in_process(dod_repo: Path) -> None:
    """In-process version of the §16 acceptance test using Click's CliRunner.

    This runs the SAME CLI surface as the subprocess test above but
    benefits from the test-suite-wide shell monkeypatch (which falls
    back from `pwsh` to `sh` on hosts without PowerShell 7). The
    subprocess version is the stronger proof that the entry point
    works end-to-end; this in-process version is the always-on
    coverage of the §16 scenario.
    """
    from click.testing import CliRunner

    from attractor.cli.main import cli

    wf = dod_repo / "wf.dot"
    wf.write_text(_DOD_WORKFLOW, encoding="utf-8")
    runner = CliRunner()

    prev_cwd = os.getcwd()
    try:
        os.chdir(dod_repo)

        # 1. validate.
        r = runner.invoke(cli, ["validate", str(wf)])
        assert r.exit_code == 0, r.output + (r.stderr or "")
        assert "valid:" in r.output

        # 2. init.
        r = runner.invoke(cli, ["init"])
        assert r.exit_code == 0

        # 3. run → pause.
        r = runner.invoke(cli, ["run", str(wf)])
        assert r.exit_code == 0, r.output + (r.stderr or "")
        assert "paused" in r.output.lower()

        # 4. discover run-id.
        repo = pygit2.Repository(str(dod_repo))
        state_branches = [
            ref for ref in repo.references
            if ref.endswith("/state") and "attractor/run/" in ref
        ]
        assert state_branches
        run_id = state_branches[0].split("/")[-2]

        # 5. list shows it.
        r = runner.invoke(cli, ["run", "list"])
        assert r.exit_code == 0

        # 6. show prints the prompt.
        r = runner.invoke(cli, ["run", "show", run_id])
        assert r.exit_code == 0
        assert run_id in r.output

        # 7. respond.
        r = runner.invoke(cli, ["run", "respond", run_id, "approve"])
        assert r.exit_code == 0

        # 8. resume → COMPLETED.
        r = runner.invoke(cli, ["run", "resume", run_id])
        assert r.exit_code == 0, r.output + (r.stderr or "")
        assert "completed" in r.output.lower()

        # 9. show after completion.
        r = runner.invoke(cli, ["run", "show", run_id])
        assert r.exit_code == 0
        assert "completed" in r.output.lower()

        # 10. (REQ-DOD-LOCAL-FIRST) attractor refs all live under
        # `refs/heads/attractor/`.
        for ref in repo.references:
            if "attractor" in ref:
                assert ref.startswith("refs/heads/attractor/"), ref

        # 11. No non-local remote.
        assert list(repo.remotes) == []

        # 12. Worktree branch persists.
        wt_branches = [
            r for r in repo.references
            if r.endswith("/worktree") and "attractor/run/" in r
        ]
        assert wt_branches
    finally:
        os.chdir(prev_cwd)


_ = os  # silence unused-import (kept for future cwd manipulation)
