from __future__ import annotations

import os
import platform
import signal
import stat
import subprocess
import threading
from pathlib import Path
from types import SimpleNamespace

import pytest

from robomp.git_ops import (
    GitCommandError,
)
from robomp.git_ops import (
    fetch_pr_head as git_fetch_pr_head,
)
from robomp.git_ops import (
    fetch_prune as git_fetch_prune,
)
from robomp.git_ops import (
    fetch_ref as git_fetch_ref,
)
from robomp.sandbox import (
    _DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT,
    SandboxManager,
    Workspace,
    _chown_workspace,
    _prepare_slot_runtime_env,
    _prepare_slot_tmpdir,
    _provision_runtime_dirs,
    _reap_slot,
    _safe_directory_env,
    _share_git_metadata_with_slots,
    _slot_pids,
    _slot_subprocess_kwargs,
    make_branch,
    rename_workspace_branch,
    workspace_key,
)


def _git(args: list[str], cwd: Path) -> None:
    subprocess.run(["git", *args], cwd=str(cwd), check=True, capture_output=True, text=True)


def _workspace(root: Path) -> Workspace:
    return Workspace(
        root=root,
        repo_dir=root / "repo",
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch="farm/test/topic",
        repo_full_name="octo/widget",
        issue_number=1,
    )


@pytest.fixture
def upstream_repo(tmp_path: Path) -> Path:
    """Create a local --bare-ish remote with one commit on main."""
    repo = tmp_path / "upstream.git"
    repo.mkdir()
    _git(["init", "--initial-branch=main", "--bare", str(repo)], cwd=tmp_path)
    seed = tmp_path / "seed"
    seed.mkdir()
    _git(["init", "--initial-branch=main", str(seed)], cwd=tmp_path)
    (seed / "README.md").write_text("hello\n", encoding="utf-8")
    _git(["-C", str(seed), "add", "."], cwd=tmp_path)
    env = os.environ | {
        "GIT_AUTHOR_NAME": "t",
        "GIT_AUTHOR_EMAIL": "t@t",
        "GIT_COMMITTER_NAME": "t",
        "GIT_COMMITTER_EMAIL": "t@t",
    }
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=str(seed),
        check=True,
        capture_output=True,
        text=True,
        env=env,
    )
    _git(["-C", str(seed), "remote", "add", "origin", str(repo)], cwd=tmp_path)
    _git(["-C", str(seed), "push", "origin", "main"], cwd=tmp_path)
    return repo


def test_workspace_key_and_branch_shape() -> None:
    assert workspace_key("oven-sh/bun", 30654) == "oven-sh__bun__30654"
    branch = make_branch(issue_number=30654, title="JSON.parse crashes on BOM", seed="oven-sh/bun#30654")
    assert branch.startswith("farm/")
    parts = branch.split("/")
    assert len(parts) == 3 and len(parts[1]) == 8
    assert "json-parse-crashes" in parts[2]


def _init_worktree_repo(repo_dir: Path, branch: str) -> None:
    """Stand up a minimal local git repo with `branch` checked out."""
    repo_dir.mkdir(parents=True, exist_ok=True)
    _git(["init", f"--initial-branch={branch}", str(repo_dir)], cwd=repo_dir.parent)
    (repo_dir / "README.md").write_text("hello\n", encoding="utf-8")
    _git(["-C", str(repo_dir), "add", "."], cwd=repo_dir.parent)
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=str(repo_dir),
        check=True,
        capture_output=True,
        text=True,
        env=os.environ
        | {
            "GIT_AUTHOR_NAME": "t",
            "GIT_AUTHOR_EMAIL": "t@t",
            "GIT_COMMITTER_NAME": "t",
            "GIT_COMMITTER_EMAIL": "t@t",
        },
    )


def test_rename_workspace_branch_renames_local_branch(tmp_path: Path) -> None:
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    initial = "farm/abc12345/some-issue"
    _init_worktree_repo(repo_dir, initial)
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    new_branch = rename_workspace_branch(ws, "fix-json-bom")
    assert new_branch == "farm/abc12345/fix-json-bom"
    assert ws.branch == "farm/abc12345/fix-json-bom"
    head = subprocess.run(
        ["git", "symbolic-ref", "HEAD"],
        cwd=str(repo_dir),
        check=True,
        capture_output=True,
        text=True,
    ).stdout.strip()
    assert head == "refs/heads/farm/abc12345/fix-json-bom"


def test_rename_workspace_branch_refreshes_shared_metadata(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    initial = "farm/abc12345/some-issue"
    _init_worktree_repo(repo_dir, initial)
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    # On Linux+root the rename runs `git branch -m` as the slot uid (2004),
    # so the worktree needs to be readable by that uid before the call.
    # On macOS dev `_slot_permissions_active` returns False and this
    # whole block is a no-op.
    if platform.system() == "Linux" and os.geteuid() == 0:
        for path in [root, repo_dir, *repo_dir.rglob("*")]:
            os.chown(path, 2004, 2004, follow_symlinks=False)
    calls: list[tuple[Path, int | None]] = []
    monkeypatch.setattr(
        "robomp.sandbox._share_git_metadata_with_slots",
        lambda repo_dir, slot_uid: calls.append((repo_dir, slot_uid)),
    )

    rename_workspace_branch(ws, "fix-json-bom", slot_uid=2004)

    assert calls == [(repo_dir, 2004)]


def test_rename_workspace_branch_runs_git_as_slot_when_permissions_active(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    repo_dir.mkdir(parents=True)
    initial = "farm/abc12345/some-issue"
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    captured: dict[str, object] = {}

    def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        captured["cmd"] = cmd
        captured["kwargs"] = kwargs
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.subprocess.run", fake_run)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda _repo_dir, _slot_uid: None)

    new_branch = rename_workspace_branch(ws, "fix-json-bom", slot_uid=2004)

    assert new_branch == "farm/abc12345/fix-json-bom"
    assert captured["cmd"] == ["git", "branch", "-m", initial, "farm/abc12345/fix-json-bom"]
    kwargs = captured["kwargs"]
    assert isinstance(kwargs, dict)
    assert kwargs["cwd"] == str(repo_dir)
    assert kwargs["user"] == 2004
    assert kwargs["group"] == 2004
    assert kwargs["extra_groups"] == [2000]


def test_rename_workspace_branch_is_idempotent_when_slug_unchanged(tmp_path: Path) -> None:
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    initial = "farm/abc12345/keep-me"
    _init_worktree_repo(repo_dir, initial)
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    # No git operation should run; nothing to rename. We assert that by
    # passing a non-existent repo_dir — the helper must not touch git.
    ws.repo_dir = tmp_path / "does-not-exist"
    out = rename_workspace_branch(ws, "keep-me")
    assert out == initial
    assert ws.branch == initial


@pytest.mark.parametrize(
    "bad",
    [
        "",
        "Has-Caps",
        "-leading",
        "trailing-",
        "double--hyphen",
        "has/slash",
        "has_underscore",
        "a" * 51,
        None,
        123,
    ],
)
def test_rename_workspace_branch_rejects_bad_slug(tmp_path: Path, bad: object) -> None:
    ws = _workspace(tmp_path / "ws")
    with pytest.raises(ValueError):
        rename_workspace_branch(ws, bad)  # type: ignore[arg-type]


def test_rename_workspace_branch_noop_when_pr_open(tmp_path: Path) -> None:
    """A non-None ``pr_number`` makes rename a no-op: an open PR on origin
    still tracks the current branch, and renaming would orphan it."""
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    initial = "farm/abc12345/old-slug"
    _init_worktree_repo(repo_dir, initial)
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    out = rename_workspace_branch(ws, "new-slug", pr_number=42)
    assert out == initial
    assert ws.branch == initial
    head = subprocess.run(
        ["git", "symbolic-ref", "HEAD"],
        cwd=str(repo_dir),
        check=True,
        capture_output=True,
        text=True,
    ).stdout.strip()
    assert head == f"refs/heads/{initial}"
    # An invalid slug must still be rejected even when pr_number suppresses the rename.
    with pytest.raises(ValueError):
        rename_workspace_branch(ws, "Bad Slug", pr_number=42)


def test_rename_workspace_branch_rejects_non_farm_branch(tmp_path: Path) -> None:
    ws = _workspace(tmp_path / "ws")
    ws.branch = "main"
    with pytest.raises(ValueError):
        rename_workspace_branch(ws, "ok-slug")


def test_rename_workspace_branch_surfaces_git_failure(tmp_path: Path) -> None:
    root = tmp_path / "ws"
    repo_dir = root / "repo"
    initial = "farm/abc12345/old"
    _init_worktree_repo(repo_dir, initial)
    # Create a second branch that collides with the rename target.
    _git(["-C", str(repo_dir), "branch", "farm/abc12345/new"], cwd=repo_dir.parent)
    ws = Workspace(
        root=root,
        repo_dir=repo_dir,
        session_dir=root / ".omp-session",
        context_dir=root / "context",
        artifacts_dir=root / "artifacts",
        branch=initial,
        repo_full_name="octo/widget",
        issue_number=1,
    )
    with pytest.raises(GitCommandError):
        rename_workspace_branch(ws, "new")
    # Original branch must remain on failure.
    assert ws.branch == initial


def test_delete_bad_refs_removes_worktree_holding_the_ref(tmp_path: Path) -> None:
    """When a bad-object ref is still checked out by a worktree, the worktree's
    stale ``HEAD`` keeps fetch failing even after ``update-ref -d`` succeeds.
    The repair MUST also tear down that worktree before deleting the ref."""
    from robomp.git_ops import _delete_bad_refs

    pool = tmp_path / "pool"
    _init_worktree_repo(pool, "main")

    # Create a worktree on `farm/badhex/bad-branch`.
    work_dir = tmp_path / "worktree"
    subprocess.run(
        ["git", "worktree", "add", "-b", "farm/badhex/bad-branch", str(work_dir), "main"],
        cwd=str(pool),
        check=True,
        capture_output=True,
        text=True,
    )
    assert (work_dir / ".git").exists()

    fetch_output = (
        "error: object directory /tmp/git-objects-aux does not exist; check .git/objects/info/alternates\n"
        "fatal: bad object refs/heads/farm/badhex/bad-branch\n"
        "error: did not send all necessary objects\n"
    )
    changed = _delete_bad_refs(pool, fetch_output)
    assert changed is True
    # Ref must be gone from the pool's refs store.
    rp = subprocess.run(
        ["git", "rev-parse", "--verify", "refs/heads/farm/badhex/bad-branch"],
        cwd=str(pool),
        capture_output=True,
        text=True,
    )
    assert rp.returncode != 0
    # And the worktree's `.git` link must be cleared so the next fetch can
    # validate connectivity without re-tripping over the dead HEAD.
    assert not (work_dir / ".git").exists()


def test_delete_bad_refs_noop_when_no_bad_ref_in_output(tmp_path: Path) -> None:
    from robomp.git_ops import _delete_bad_refs

    pool = tmp_path / "pool"
    _init_worktree_repo(pool, "main")
    # Output that doesn't match the bad-object regex.
    assert _delete_bad_refs(pool, "fatal: unrelated failure\n") is False


def test_ensure_workspace_creates_worktree(tmp_path: Path, upstream_repo: Path) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=42,
        title="something is wrong",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    assert ws.repo_dir.is_dir()
    assert (ws.repo_dir / "README.md").read_text() == "hello\n"
    # Branch is checked out.
    result = subprocess.run(
        ["git", "-C", str(ws.repo_dir), "rev-parse", "--abbrev-ref", "HEAD"],
        capture_output=True,
        text=True,
        check=True,
    )
    assert result.stdout.strip() == ws.branch
    assert ws.branch.startswith("farm/")
    # Session and context dirs exist.
    assert ws.session_dir.is_dir()
    assert ws.context_dir.is_dir()
    assert ws.repro_dir.is_dir()
    assert ws.artifacts_dir.is_dir()


def test_ensure_workspace_pr_head_uses_detached_pr_ref(tmp_path: Path, upstream_repo: Path) -> None:
    contributor = tmp_path / "contributor"
    _git(["clone", str(upstream_repo), str(contributor)], cwd=tmp_path)
    (contributor / "README.md").write_text("hello from pr\n", encoding="utf-8")
    _git(["-C", str(contributor), "add", "README.md"], cwd=tmp_path)
    subprocess.run(
        ["git", "commit", "-m", "pr change"],
        cwd=str(contributor),
        check=True,
        capture_output=True,
        text=True,
        env=os.environ
        | {
            "GIT_AUTHOR_NAME": "c",
            "GIT_AUTHOR_EMAIL": "c@t",
            "GIT_COMMITTER_NAME": "c",
            "GIT_COMMITTER_EMAIL": "c@t",
        },
    )
    pr_head = subprocess.run(
        ["git", "rev-parse", "HEAD"],
        cwd=str(contributor),
        check=True,
        capture_output=True,
        text=True,
    ).stdout.strip()
    _git(["-C", str(contributor), "push", "origin", "HEAD:refs/pull/9/head"], cwd=tmp_path)

    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=9,
        title="incoming PR",
        clone_url=str(upstream_repo),
        default_branch="main",
        pr_head=9,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    head = subprocess.run(
        ["git", "rev-parse", "HEAD"],
        cwd=str(ws.repo_dir),
        check=True,
        capture_output=True,
        text=True,
    ).stdout.strip()
    symbolic = subprocess.run(
        ["git", "symbolic-ref", "--quiet", "--short", "HEAD"],
        cwd=str(ws.repo_dir),
        capture_output=True,
        text=True,
        check=False,
    )
    pushurl = subprocess.run(
        ["git", "config", "--get", "remote.origin.pushurl"],
        cwd=str(ws.repo_dir),
        capture_output=True,
        text=True,
        check=False,
    )
    assert head == pr_head
    assert symbolic.returncode != 0
    assert ws.branch == "review/pr-9"
    assert pushurl.returncode != 0


def test_chown_workspace_noops_when_not_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[list[str], bool]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 1000)
    monkeypatch.setattr(
        "robomp.sandbox.subprocess.run",
        lambda cmd, *, check: calls.append((cmd, check)),
    )

    _chown_workspace(tmp_path, 2001)

    assert calls == []


def test_chown_workspace_noops_off_linux(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[list[str], bool]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Darwin")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr(
        "robomp.sandbox.subprocess.run",
        lambda cmd, *, check: calls.append((cmd, check)),
    )

    _chown_workspace(tmp_path, 2001)

    assert calls == []


def test_chown_workspace_runs_chown_and_chmod_as_root_on_linux(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[list[str], bool]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)

    def fake_run(cmd, *, check, timeout):
        assert timeout == _DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT
        calls.append((cmd, check))

    monkeypatch.setattr("robomp.sandbox.subprocess.run", fake_run)

    _chown_workspace(tmp_path, 2001)

    # 2001 is the slot-private GID matching the slot UID, not the shared omp group.
    assert calls == [
        (["chown", "-R", "2001:2001", str(tmp_path)], True),
        (["chmod", "-R", "u=rwX,g=rwX,o=", str(tmp_path)], True),
    ]


def test_chown_workspace_makes_workspace_slot_owned(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    subdir = tmp_path / "subdir"
    subdir.mkdir()
    file_path = subdir / "file.txt"
    file_path.write_text("data\n", encoding="utf-8")
    tmp_path.chmod(0o777)
    subdir.chmod(0o777)
    file_path.chmod(0o777)
    owned: dict[Path, tuple[int, int]] = {}

    def fake_run(cmd: list[str], *, check: bool, timeout: float) -> None:
        assert check
        assert timeout == _DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT
        if cmd[:2] == ["chown", "-R"]:
            uid_text, gid_text = cmd[2].split(":", 1)
            root = Path(cmd[3])
            uid = int(uid_text)
            gid = int(gid_text)
            owned[root] = (uid, gid)
            for current_root, dirs, files in os.walk(root):
                current = Path(current_root)
                owned[current] = (uid, gid)
                for dirname in dirs:
                    owned[current / dirname] = (uid, gid)
                for filename in files:
                    owned[current / filename] = (uid, gid)
        elif cmd[:3] == ["chmod", "-R", "u=rwX,g=rwX,o="]:
            root = Path(cmd[3])
            root.chmod(0o770)
            for current_root, dirs, files in os.walk(root):
                current = Path(current_root)
                current.chmod(0o770)
                for dirname in dirs:
                    (current / dirname).chmod(0o770)
                for filename in files:
                    (current / filename).chmod(0o660)
        else:
            raise AssertionError(f"unexpected command: {cmd!r}")

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.subprocess.run", fake_run)

    _chown_workspace(tmp_path, 2001)

    assert owned[tmp_path] == (2001, 2001)
    assert owned[subdir] == (2001, 2001)
    assert owned[file_path] == (2001, 2001)
    assert stat.S_IMODE(tmp_path.stat().st_mode) == 0o770
    assert stat.S_IMODE(subdir.stat().st_mode) == 0o770
    assert stat.S_IMODE(file_path.stat().st_mode) == 0o660


def test_slot_pids_reads_proc_status_and_skips_zombies(tmp_path: Path) -> None:
    nonnumeric = tmp_path / "self"
    nonnumeric.mkdir()

    live = tmp_path / "123"
    live.mkdir()
    (live / "status").write_text(
        "Name:\tomp\nState:\tS (sleeping)\nUid:\t0\t2001\t2001\t2001\n",
        encoding="utf-8",
    )

    zombie = tmp_path / "124"
    zombie.mkdir()
    (zombie / "status").write_text(
        "Name:\tomp\nState:\tZ (zombie)\nUid:\t2001\t2001\t2001\t2001\n",
        encoding="utf-8",
    )

    other = tmp_path / "125"
    other.mkdir()
    (other / "status").write_text(
        "Name:\troot\nState:\tS (sleeping)\nUid:\t0\t0\t0\t0\n",
        encoding="utf-8",
    )

    assert _slot_pids(2001, tmp_path) == (123,)


def test_reap_slot_noops_when_permissions_inactive(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[int, int]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Darwin")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.os.kill", lambda pid, sig: calls.append((pid, sig)))

    _reap_slot(2001)

    assert calls == []


def test_reap_slot_kills_slot_uid_on_linux_root(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[int, int]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox._slot_pids", lambda _uid: (111, 222))
    monkeypatch.setattr("robomp.sandbox.os.kill", lambda pid, sig: calls.append((pid, sig)))

    _reap_slot(2001)

    assert calls == [(111, signal.SIGKILL), (222, signal.SIGKILL)]


def test_prepare_slot_tmpdir_mkdirs_without_chown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    chowns: list[tuple[Path, int, int]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.os.chown", lambda path, uid, gid: chowns.append((Path(path), uid, gid)))

    tmpdir = _prepare_slot_tmpdir(_workspace(tmp_path), 2001)

    assert tmpdir == tmp_path / ".omp-tmp"
    assert tmpdir.is_dir()
    assert stat.S_IMODE(tmpdir.stat().st_mode) == 0o700
    assert chowns == []


def test_prepare_slot_tmpdir_replaces_symlink_without_touching_target(tmp_path: Path) -> None:
    target = tmp_path / "target"
    target.mkdir()
    tmpdir = tmp_path / ".omp-tmp"
    tmpdir.symlink_to(target, target_is_directory=True)

    prepared = _prepare_slot_tmpdir(_workspace(tmp_path), None)

    assert prepared == tmpdir
    assert prepared.is_dir()
    assert not prepared.is_symlink()
    assert target.is_dir()


def test_provision_runtime_dirs_replaces_tmpdir_symlink_and_creates_xdg_tree(tmp_path: Path) -> None:
    target = tmp_path / "target"
    target.mkdir()
    tmpdir = tmp_path / ".omp-tmp"
    tmpdir.symlink_to(target, target_is_directory=True)

    _provision_runtime_dirs(tmp_path)

    assert tmpdir.is_dir()
    assert not tmpdir.is_symlink()
    assert target.is_dir()
    assert stat.S_IMODE(tmpdir.stat().st_mode) == 0o700
    for base in (tmp_path / ".omp-xdg" / "data", tmp_path / ".omp-xdg" / "state", tmp_path / ".omp-xdg" / "cache"):
        assert base.is_dir()
        assert (base / "omp").is_dir()
    assert (tmp_path / ".omp-xdg" / "cache" / "bun-install").is_dir()


def test_safe_directory_env_scopes_single_repo_path(tmp_path: Path) -> None:
    repo_dir = tmp_path / "repo"

    assert _safe_directory_env(repo_dir) == {
        "GIT_CONFIG_COUNT": "1",
        "GIT_CONFIG_KEY_0": "safe.directory",
        "GIT_CONFIG_VALUE_0": str(repo_dir),
    }


def test_slot_subprocess_kwargs_run_as_slot_on_linux_root(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)

    assert _slot_subprocess_kwargs(2001) == {
        "user": 2001,
        "group": 2001,
        "extra_groups": [2000],
        "umask": 0o002,
    }


def test_chown_workspace_normalizes_to_root_when_slots_disabled(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    calls: list[tuple[list[str], bool | None]] = []

    def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append((cmd, kwargs.get("check") if isinstance(kwargs.get("check"), bool) else None))
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.os.getegid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.subprocess.run", fake_run)

    _chown_workspace(tmp_path, None)

    assert calls == [
        (["chown", "-R", "0:0", str(tmp_path)], True),
        (["chmod", "-R", "u=rwX,g=rwX,o=", str(tmp_path)], True),
    ]


def test_prepare_slot_runtime_env_returns_workspace_private_paths_without_chown(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    chowns: list[tuple[Path, int, int]] = []
    calls: list[list[str]] = []

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.os.chown", lambda path, uid, gid: chowns.append((Path(path), uid, gid)))
    monkeypatch.setattr("robomp.sandbox.subprocess.run", lambda cmd, **_kwargs: calls.append(cmd))

    ws = _workspace(tmp_path)
    bun_cache = ws.root / ".omp-xdg" / "cache" / "bun-install"

    env = _prepare_slot_runtime_env(ws, 2001)

    assert env["TMPDIR"] == str(ws.root / ".omp-tmp")
    assert env["XDG_CACHE_HOME"] == str(ws.root / ".omp-xdg" / "cache")
    assert env["BUN_INSTALL_CACHE_DIR"] == str(bun_cache)
    for base in (ws.root / ".omp-xdg" / "data", ws.root / ".omp-xdg" / "state", ws.root / ".omp-xdg" / "cache"):
        assert base.is_dir()
        assert (base / "omp").is_dir()
    assert bun_cache.is_dir()
    assert chowns == []
    assert calls == []


def test_share_git_metadata_keeps_pool_writable_for_retry_slot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    repo_dir = tmp_path / "workspaces" / "octo__widget__43" / "repo"
    repo_dir.mkdir(parents=True)
    common_dir = tmp_path / "workspaces" / "_pool" / "octo__widget" / ".git"
    git_dir = common_dir / "worktrees" / "repo"
    git_dir.mkdir(parents=True)
    (repo_dir / ".git").write_text(f"gitdir: {git_dir}\n", encoding="utf-8")
    (git_dir / "commondir").write_text("../..\n", encoding="utf-8")

    object_dir = common_dir / "objects" / "ab"
    object_dir.mkdir(parents=True)
    object_file = object_dir / "object"
    object_file.write_text("object\n", encoding="utf-8")
    object_file.chmod(0o600)

    ref_dir = common_dir / "refs" / "heads"
    ref_dir.mkdir(parents=True)
    ref_file = ref_dir / "farm"
    ref_file.write_text("sha\n", encoding="utf-8")
    ref_file.chmod(0o600)

    log_dir = common_dir / "logs" / "refs" / "heads"
    log_dir.mkdir(parents=True)
    log_file = log_dir / "farm"
    log_file.write_text("sha sha bot <bot@example.invalid> commit\n", encoding="utf-8")
    log_file.chmod(0o600)

    index_file = git_dir / "index"
    index_file.write_text("index\n", encoding="utf-8")
    index_file.chmod(0o600)

    chowns: list[tuple[Path, int, int]] = []
    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.os.chown", lambda path, uid, gid: chowns.append((Path(path), uid, gid)))

    _share_git_metadata_with_slots(repo_dir, 2002)

    assert object_dir.stat().st_mode & stat.S_IWGRP
    assert object_dir.stat().st_mode & stat.S_ISGID
    assert object_file.stat().st_mode & stat.S_IRGRP
    assert not object_file.stat().st_mode & stat.S_IWGRP
    assert ref_file.stat().st_mode & stat.S_IWGRP
    assert log_file.stat().st_mode & stat.S_IWGRP
    assert index_file.stat().st_mode & stat.S_IWGRP
    assert (git_dir, -1, 2000) in chowns


def test_ensure_workspace_refreshes_permissions_for_retry_slot_and_session(
    tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    chowns: list[tuple[Path, int | None]] = []
    shared: list[tuple[Path, int | None]] = []
    real_chown = _chown_workspace
    real_share = _share_git_metadata_with_slots

    def record_chown(root: Path, slot_uid: int | None) -> None:
        chowns.append((root, slot_uid))
        # Delegate so subsequent slot-identity git ops can stat the tree.
        real_chown(root, slot_uid)

    def record_share(repo_dir: Path, slot_uid: int | None) -> None:
        shared.append((repo_dir, slot_uid))
        real_share(repo_dir, slot_uid)

    monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", record_share)

    mgr = SandboxManager(tmp_path / "workspaces")
    ws1 = mgr.ensure_workspace(
        repo="octo/widget",
        number=44,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=2001,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    transcript = ws1.session_dir / "turn.jsonl"
    transcript.write_text("{}\n", encoding="utf-8")
    ws2 = mgr.ensure_workspace(
        repo="octo/widget",
        number=44,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        existing_branch=ws1.branch,
        slot_uid=2002,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert ws2.repo_dir == ws1.repo_dir
    assert ws2.session_dir == ws1.session_dir
    assert transcript.is_file()
    assert ws2.branch == ws1.branch
    assert shared == [
        (ws1.repo_dir, 2001),
        (ws1.repo_dir, 2001),
        (ws1.repo_dir, 2002),
        (ws1.repo_dir, 2002),
    ]
    assert chowns == [(ws1.root, 2001), (ws1.root, 2002)]


def test_ensure_workspace_preserves_checked_out_branch_on_replay(tmp_path: Path, upstream_repo: Path) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws1 = mgr.ensure_workspace(
        repo="octo/widget",
        number=45,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=None,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    renamed = "farm/abc12345/renamed"
    _git(["-C", str(ws1.repo_dir), "branch", "-m", ws1.branch, renamed], cwd=ws1.repo_dir.parent)

    ws2 = mgr.ensure_workspace(
        repo="octo/widget",
        number=45,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=None,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert ws2.branch == renamed


def test_ensure_workspace_runs_existing_worktree_git_as_slot_after_chown(
    tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws1 = mgr.ensure_workspace(
        repo="octo/widget",
        number=47,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=None,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    events: list[tuple[str, int | None]] = []
    git_calls: list[tuple[list[str], dict[str, object]]] = []

    def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        git_calls.append((cmd, kwargs))
        if cmd[:3] == ["git", "remote", "get-url"]:
            return subprocess.CompletedProcess(cmd, 0, f"{upstream_repo}\n", "")
        if cmd[:4] == ["git", "symbolic-ref", "--quiet", "--short"]:
            user = kwargs.get("user")
            events.append(("symbolic-ref", user if isinstance(user, int) else None))
            return subprocess.CompletedProcess(cmd, 0, f"{ws1.branch}\n", "")
        if cmd[:2] == ["git", "config"]:
            user = kwargs.get("user")
            events.append(("config", user if isinstance(user, int) else None))
        return subprocess.CompletedProcess(cmd, 0, "", "")

    def record_chown(_ws_root: Path, slot_uid: int | None) -> None:
        events.append(("chown", slot_uid))

    monkeypatch.setattr("robomp.sandbox.platform.system", lambda: "Linux")
    monkeypatch.setattr("robomp.sandbox.os.geteuid", lambda: 0)
    monkeypatch.setattr("robomp.sandbox.subprocess.run", fake_run)
    monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda _repo_dir, _slot_uid: None)

    ws2 = mgr.ensure_workspace(
        repo="octo/widget",
        number=47,
        title="retry me",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=2002,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert ws2.branch == ws1.branch
    assert events[0] == ("chown", 2002)
    assert ("symbolic-ref", 2002) in events
    assert events.index(("chown", 2002)) < events.index(("symbolic-ref", 2002))
    assert events.count(("config", 2002)) == 2
    worktree_git = [kwargs for cmd, kwargs in git_calls if cmd[:2] in (["git", "symbolic-ref"], ["git", "config"])]
    assert worktree_git
    assert all(kwargs["user"] == 2002 and kwargs["group"] == 2002 for kwargs in worktree_git)


def test_ensure_workspace_invokes_slot_chown(
    tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    calls: list[tuple[Path, int | None]] = []
    real_chown = _chown_workspace

    def record_chown(ws_root: Path, slot_uid: int | None) -> None:
        calls.append((ws_root, slot_uid))
        # Delegate to the real chown so the subsequent `git config` as the
        # slot can stat the tree. On macOS dev (uid != 0) the real chown is
        # itself a no-op; on Linux+root in CI it hands the tree to the slot.
        real_chown(ws_root, slot_uid)

    monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
    mgr = SandboxManager(tmp_path / "workspaces")

    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=43,
        title="something is wrong",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=2001,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert calls == [(ws.root, 2001)]


def test_ensure_workspace_provisions_and_slot_owns_runtime_dirs(
    tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    owned: dict[Path, tuple[int, int]] = {}
    runtime_paths: list[Path] = []
    real_chown = _chown_workspace

    def record_chown(ws_root: Path, slot_uid: int | None) -> None:
        assert slot_uid is not None
        paths = [
            ws_root / ".omp-tmp",
            ws_root / ".omp-xdg" / "data",
            ws_root / ".omp-xdg" / "data" / "omp",
            ws_root / ".omp-xdg" / "state",
            ws_root / ".omp-xdg" / "state" / "omp",
            ws_root / ".omp-xdg" / "cache",
            ws_root / ".omp-xdg" / "cache" / "omp",
            ws_root / ".omp-xdg" / "cache" / "bun-install",
        ]
        runtime_paths.extend(paths)
        for path in paths:
            assert path.is_dir()
            owned[path] = (slot_uid, slot_uid)
        # Same rationale as test_ensure_workspace_invokes_slot_chown: hand
        # the tree to the slot so the subsequent `git config` works under
        # real slot permissions in CI.
        real_chown(ws_root, slot_uid)

    monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
    mgr = SandboxManager(tmp_path / "workspaces")

    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=46,
        title="runtime perms",
        clone_url=str(upstream_repo),
        default_branch="main",
        slot_uid=2001,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert runtime_paths
    assert set(runtime_paths) == {
        ws.root / ".omp-tmp",
        ws.root / ".omp-xdg" / "data",
        ws.root / ".omp-xdg" / "data" / "omp",
        ws.root / ".omp-xdg" / "state",
        ws.root / ".omp-xdg" / "state" / "omp",
        ws.root / ".omp-xdg" / "cache",
        ws.root / ".omp-xdg" / "cache" / "omp",
        ws.root / ".omp-xdg" / "cache" / "bun-install",
    }
    assert set(owned.values()) == {(2001, 2001)}


def test_ensure_workspace_is_idempotent(tmp_path: Path, upstream_repo: Path) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws1 = mgr.ensure_workspace(
        repo="octo/widget",
        number=5,
        title="t",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    ws2 = mgr.ensure_workspace(
        repo="octo/widget",
        number=5,
        title="t",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    assert ws1.repo_dir == ws2.repo_dir
    assert ws1.branch == ws2.branch


def test_ensure_workspace_existing_branch_starts_from_remote_head(tmp_path: Path, upstream_repo: Path) -> None:
    branch = "farm/abc12345/existing-pr"
    seed = tmp_path / "remote-branch-seed"
    _git(["clone", str(upstream_repo), str(seed)], cwd=tmp_path)
    _git(["-C", str(seed), "checkout", "-b", branch], cwd=tmp_path)
    (seed / "README.md").write_text("from pr branch\n", encoding="utf-8")
    _git(["-C", str(seed), "add", "README.md"], cwd=tmp_path)
    subprocess.run(
        ["git", "commit", "-m", "pr branch"],
        cwd=str(seed),
        check=True,
        capture_output=True,
        text=True,
        env=os.environ
        | {
            "GIT_AUTHOR_NAME": "t",
            "GIT_AUTHOR_EMAIL": "t@t",
            "GIT_COMMITTER_NAME": "t",
            "GIT_COMMITTER_EMAIL": "t@t",
        },
    )
    _git(["-C", str(seed), "push", "origin", branch], cwd=tmp_path)

    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=77,
        title="follow up",
        clone_url=str(upstream_repo),
        default_branch="main",
        existing_branch=branch,
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )

    assert ws.branch == branch
    assert (ws.repo_dir / "README.md").read_text(encoding="utf-8") == "from pr branch\n"


def test_remove_workspace(tmp_path: Path, upstream_repo: Path) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=12,
        title="t",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    assert ws.repo_dir.exists()
    mgr.remove_workspace(repo="octo/widget", number=12)
    assert not ws.repo_dir.exists()
    assert not ws.root.exists()


def test_remove_workspace_prunes_pool_after_failed_worktree_remove(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    mgr = SandboxManager(tmp_path)
    # Create a real repo_dir on disk so `repo_dir.exists()` is True on entry.
    ws_root = mgr.workspace_root("o/r", 7)
    repo_dir = ws_root / "repo"
    repo_dir.mkdir(parents=True, exist_ok=True)
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)
    (pool / ".git").mkdir(exist_ok=True)  # mark as a real git pool for the cleanup gate

    calls: list[tuple[list[str], object]] = []

    def fake_safe_run(cmd, **k):
        calls.append((list(cmd), k.get("cwd")))
        # The `worktree remove` "times out" (124) and does NOT delete repo_dir,
        # so the code must fall back to rmtree + prune. `prune` succeeds (0).
        if cmd[:3] == ["git", "worktree", "remove"]:
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    mgr.remove_workspace(repo="o/r", number=7)

    cmds = [c for c, _ in calls]
    # The dangling registration must have been pruned, in the correct pool.
    assert ["git", "worktree", "prune"] in cmds, (
        "remove_workspace did not prune pool metadata after a failed worktree remove"
    )
    prune_idx = next(i for i, (c, _) in enumerate(calls) if c == ["git", "worktree", "prune"])
    assert calls[prune_idx][1] == pool, "prune did not run in the repo's pool dir"
    # And the remove was attempted first (ordering: remove before prune).
    remove_idx = next(i for i, (c, _) in enumerate(calls) if c[:3] == ["git", "worktree", "remove"])
    assert remove_idx < prune_idx
    # The real checkout dir was cleaned up.
    assert not repo_dir.exists()


def test_remove_workspace_prunes_when_failed_remove_already_deleted_checkout(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # The reviewer's case: `git worktree remove` deletes the checkout dir but is
    # killed (124) before clearing the pool's worktree registration. `repo_dir`
    # is gone by the time we check, so a guard on `repo_dir.exists()` would skip
    # the prune and leave dangling metadata; guarding on the return code must not.
    mgr = SandboxManager(tmp_path)
    ws_root = mgr.workspace_root("o/r", 9)
    repo_dir = ws_root / "repo"
    repo_dir.mkdir(parents=True, exist_ok=True)
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)
    (pool / ".git").mkdir(exist_ok=True)  # mark as a real git pool for the cleanup gate

    calls: list[tuple[list[str], object]] = []

    def fake_safe_run(cmd, **k):
        calls.append((list(cmd), k.get("cwd")))
        if cmd[:3] == ["git", "worktree", "remove"]:
            # Simulate git deleting the checkout, then dying before it could
            # unregister the worktree from the pool.
            repo_dir.rmdir()
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    mgr.remove_workspace(repo="o/r", number=9)

    cmds = [c for c, _ in calls]
    assert ["git", "worktree", "prune"] in cmds, (
        "prune was skipped after a failed remove that had already deleted the checkout"
    )
    prune_idx = next(i for i, (c, _) in enumerate(calls) if c == ["git", "worktree", "prune"])
    assert calls[prune_idx][1] == pool, "prune did not run in the repo's pool dir"

def test_redact_credentials_strips_userinfo() -> None:
    from robomp.sandbox import redact_credentials

    assert (
        redact_credentials("Cloning into 'x' from https://bot:ghp_secret@github.com/o/r.git failed")
        == "Cloning into 'x' from https://***@github.com/o/r.git failed"
    )
    # Multiple URLs in one string.
    assert (
        redact_credentials("a https://x:y@example.com b https://q:z@example.org c")
        == "a https://***@example.com b https://***@example.org c"
    )
    # No-op on strings without credentials.
    assert redact_credentials("plain message") == "plain message"
    assert redact_credentials(None) == ""


def test_git_command_error_redacts_url_in_args_and_stderr(tmp_path: Path) -> None:
    """An ENOENT-style git failure on a credentialed clone URL must not echo the token."""
    import pytest as _pytest

    from robomp.sandbox import _run

    cred_url = "https://bot:ghp_abc123secret@example.invalid/o/r.git"
    with _pytest.raises(Exception) as exc:
        _run(["git", "clone", cred_url, str(tmp_path / "out")])
    text = str(exc.value)
    assert "ghp_abc123secret" not in text
    assert "bot" not in text or "https://bot:" not in text
    assert "***" in text or "example.invalid" in text


def test_ensure_workspace_rewrites_credentialed_origin(tmp_path: Path, upstream_repo: Path) -> None:
    """A pool clone created by an older deploy with `https://user:pass@…` in
    `.git/config` must have its `origin` URL rewritten to the credential-free
    URL before the next fetch — credentials NEVER persist on disk."""
    mgr = SandboxManager(tmp_path / "workspaces")
    # Pre-seed the pool by hand, simulating an older deploy: clone, then
    # rewrite `origin` to a credentialed URL pointing at the same local bare.
    pool = mgr.pool_path("octo/widget")
    pool.parent.mkdir(parents=True, exist_ok=True)
    _git(["clone", "--filter=blob:none", str(upstream_repo), str(pool)], cwd=tmp_path)
    credentialed = "https://bot:ghp_seekrit@example.invalid/octo/widget.git"
    _git(["-C", str(pool), "remote", "set-url", "origin", credentialed], cwd=tmp_path)
    config = (pool / ".git" / "config").read_text()
    assert "ghp_seekrit" in config  # sanity: precondition

    # Now resolve through ensure_workspace using the clean URL we now own.
    # The fetch step itself will fail against the bogus example.invalid host,
    # so route through a clean local URL by setting it as the canonical
    # clone_url; the remote MUST be rewritten BEFORE fetch.
    mgr.ensure_workspace(
        repo="octo/widget",
        number=7,
        title="t",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    config_after = (pool / ".git" / "config").read_text()
    assert "ghp_seekrit" not in config_after, config_after
    assert "bot:" not in config_after, config_after
    # Origin now points at the clean URL.
    url = subprocess.run(
        ["git", "-C", str(pool), "remote", "get-url", "origin"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout.strip()
    assert url == str(upstream_repo)


def test_push_force_with_lease_succeeds_after_local_amend(tmp_path: Path, upstream_repo: Path) -> None:
    """An agent amending an already-pushed commit (e.g. `--reset-author`) must
    still be able to push: `--force-with-lease` allows the local rewrite as
    long as origin still matches what we last fetched."""
    from robomp.git_ops import push as git_push

    # Clone, make a commit, push, then amend and push again.
    work = tmp_path / "work"
    _git(["clone", str(upstream_repo), str(work)], cwd=tmp_path)
    _git(["-C", str(work), "config", "user.email", "t@t"], cwd=tmp_path)
    _git(["-C", str(work), "config", "user.name", "t"], cwd=tmp_path)
    _git(["-C", str(work), "checkout", "-b", "farm/abc/topic"], cwd=tmp_path)
    (work / "x.txt").write_text("a\n")
    _git(["-C", str(work), "add", "x.txt"], cwd=tmp_path)
    _git(["-C", str(work), "commit", "-m", "initial"], cwd=tmp_path)
    git_push(work, branch="farm/abc/topic", expected_head=None, token=None)

    # Amend (rewrites the SHA at origin/farm/abc/topic).
    (work / "x.txt").write_text("a-amended\n")
    _git(["-C", str(work), "add", "x.txt"], cwd=tmp_path)
    _git(["-C", str(work), "commit", "--amend", "--no-edit"], cwd=tmp_path)
    amended = subprocess.run(
        ["git", "-C", str(work), "rev-parse", "HEAD"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout.strip()
    result = git_push(work, branch="farm/abc/topic", expected_head=None, token=None)
    assert result.head == amended

    # Origin's branch ref now matches the amended SHA.
    on_origin = subprocess.run(
        ["git", "-C", str(upstream_repo), "rev-parse", "refs/heads/farm/abc/topic"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout.strip()
    assert on_origin == amended


def test_push_force_with_lease_refuses_when_origin_moved(tmp_path: Path, upstream_repo: Path) -> None:
    """If origin's branch ref has been moved by some other writer between our
    last fetch and this push, the lease MUST refuse — even though we're
    force-pushing."""
    from robomp.git_ops import GitCommandError
    from robomp.git_ops import push as git_push

    work = tmp_path / "work"
    _git(["clone", str(upstream_repo), str(work)], cwd=tmp_path)
    _git(["-C", str(work), "config", "user.email", "t@t"], cwd=tmp_path)
    _git(["-C", str(work), "config", "user.name", "t"], cwd=tmp_path)
    _git(["-C", str(work), "checkout", "-b", "farm/abc/topic"], cwd=tmp_path)
    (work / "x.txt").write_text("a\n")
    _git(["-C", str(work), "add", "x.txt"], cwd=tmp_path)
    _git(["-C", str(work), "commit", "-m", "initial"], cwd=tmp_path)
    git_push(work, branch="farm/abc/topic", expected_head=None, token=None)

    # A "sneaky" second writer publishes a different SHA to the same ref on
    # origin — pushed from an independent worktree, NOT seen by `work`'s
    # remote-tracking ref.
    intruder = tmp_path / "intruder"
    _git(["clone", str(upstream_repo), str(intruder)], cwd=tmp_path)
    _git(["-C", str(intruder), "config", "user.email", "i@i"], cwd=tmp_path)
    _git(["-C", str(intruder), "config", "user.name", "i"], cwd=tmp_path)
    _git(["-C", str(intruder), "checkout", "-b", "farm/abc/topic", "origin/farm/abc/topic"], cwd=tmp_path)
    (intruder / "x.txt").write_text("from-intruder\n")
    _git(["-C", str(intruder), "add", "x.txt"], cwd=tmp_path)
    _git(["-C", str(intruder), "commit", "--amend", "--no-edit"], cwd=tmp_path)
    _git(["-C", str(intruder), "push", "--force", "origin", "farm/abc/topic"], cwd=tmp_path)

    # Now `work` tries to push another amended commit. The lease pins the
    # expected origin SHA to whatever `work`'s remote-tracking ref still
    # records — which is now stale — so origin's actual SHA differs and the
    # push must be refused.
    (work / "x.txt").write_text("from-us\n")
    _git(["-C", str(work), "add", "x.txt"], cwd=tmp_path)
    _git(["-C", str(work), "commit", "--amend", "--no-edit"], cwd=tmp_path)
    with pytest.raises(GitCommandError) as exc:
        git_push(work, branch="farm/abc/topic", expected_head=None, token=None)
    assert (
        "stale info" in (exc.value.stderr + exc.value.stdout).lower()
        or "rejected" in (exc.value.stderr + exc.value.stdout).lower()
    )


def test_run_git_injects_safe_directory_and_subprocess_identity(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    from robomp.git_ops import _run_git

    captured: dict[str, object] = {}

    def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        captured["cmd"] = cmd
        captured.update(kwargs)
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.git_ops.subprocess.run", fake_run)

    _run_git(
        ["status"],
        cwd=tmp_path,
        token=None,
        safe_directory=Path("/x"),
        user=2001,
        group=2001,
        extra_groups=[2000],
        umask=0o002,
    )

    env = captured["env"]
    assert isinstance(env, dict)
    assert env["GIT_CONFIG_COUNT"] == "1"
    assert env["GIT_CONFIG_KEY_0"] == "safe.directory"
    assert env["GIT_CONFIG_VALUE_0"] == "/x"
    cmd = captured["cmd"]
    assert isinstance(cmd, list)
    assert "protocol.ext.allow=never" in cmd
    assert captured["user"] == 2001
    assert captured["group"] == 2001
    assert captured["extra_groups"] == [2000]
    assert captured["umask"] == 0o002


def test_run_git_scopes_token_and_scrubs_parent_auth_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    from robomp.git_ops import AUTH_ENV_VAR, _run_git

    captured: dict[str, object] = {}

    def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        captured["cmd"] = cmd
        captured.update(kwargs)
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setenv("GITHUB_TOKEN", "parent-token")
    monkeypatch.setenv(AUTH_ENV_VAR, "parent-auth")
    monkeypatch.setattr("robomp.git_ops.subprocess.run", fake_run)
    auth_url = "https://github.com/octo/widget.git"

    _run_git(["ls-remote", auth_url], cwd=tmp_path, token="scoped-token", auth_url=auth_url)

    env = captured["env"]
    cmd = captured["cmd"]
    assert isinstance(env, dict)
    assert isinstance(cmd, list)
    assert env[AUTH_ENV_VAR].startswith("Authorization: Basic ")
    assert env[AUTH_ENV_VAR] != "parent-auth"
    assert "GITHUB_TOKEN" not in env
    assert env["GIT_ALLOW_PROTOCOL"] == "https"
    assert env["GIT_CONFIG_NOSYSTEM"] == "1"
    assert "--config-env" in cmd
    assert f"http.{auth_url}.extraHeader={AUTH_ENV_VAR}" in cmd
    assert "protocol.allow=never" in cmd
    assert "protocol.https.allow=always" in cmd
    assert "protocol.ext.allow=never" in cmd
    assert "core.hooksPath=/dev/null" in cmd
    assert "http.proxy=" in cmd
    assert "http.sslVerify=true" in cmd
    assert f"http.{auth_url}.proxy=" in cmd
    assert f"http.{auth_url}.sslVerify=true" in cmd
    assert f"http.{auth_url}.extraHeader=" in cmd


def test_run_git_kills_hung_child(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """A `git` invocation that hangs past the timeout must be killed and
    raised as `GitCommandError(124)` rather than pinning the calling
    thread. The proxy already bounds the async caller via
    `asyncio.wait_for`, but the OS process only goes away because of this
    timeout."""
    from robomp.git_ops import GitCommandError, _run_git

    fakebin = tmp_path / "bin"
    fakebin.mkdir()
    fake_git = fakebin / "git"
    # Use `exec /bin/sleep 30` so the kill from `subprocess.run`'s timeout
    # actually terminates the wait — `sh` with a non-exec `sleep` would
    # keep the parent alive on SIGTERM, and the absolute path means the
    # shim doesn't depend on PATH (we point PATH at fakebin so `git`
    # itself resolves to our shim).
    fake_git.write_text("#!/bin/sh\nexec /bin/sleep 30\n")
    fake_git.chmod(0o755)
    monkeypatch.setenv("PATH", str(fakebin))

    with pytest.raises(GitCommandError) as exc:
        _run_git(["status"], cwd=tmp_path, token=None, timeout=0.5)
    assert exc.value.returncode == 124
    assert "timed out" in exc.value.stderr.lower()


# ---------------------------------------------------------------------------
# Partial-clone blob backfill (oh-my-pi#1818)
# ---------------------------------------------------------------------------


def _partial_clone_upstream(tmp_path: Path) -> Path:
    """Bare upstream that advertises ``uploadpack.allowFilter`` so partial
    clones over ``file://`` actually skip blobs (local-protocol clones
    otherwise ignore ``--filter``)."""
    repo = tmp_path / "partial-upstream.git"
    repo.mkdir()
    _git(["init", "--initial-branch=main", "--bare", str(repo)], cwd=tmp_path)
    _git(["-C", str(repo), "config", "uploadpack.allowFilter", "true"], cwd=tmp_path)
    _git(["-C", str(repo), "config", "uploadpack.allowAnySHA1InWant", "true"], cwd=tmp_path)
    seed = tmp_path / "partial-seed"
    seed.mkdir()
    _git(["init", "--initial-branch=main", str(seed)], cwd=tmp_path)
    (seed / "README.md").write_text("hello\n", encoding="utf-8")
    _git(["-C", str(seed), "add", "."], cwd=tmp_path)
    subprocess.run(
        ["git", "-C", str(seed), "commit", "-m", "init"],
        check=True,
        capture_output=True,
        text=True,
        env=os.environ
        | {
            "GIT_AUTHOR_NAME": "t",
            "GIT_AUTHOR_EMAIL": "t@t",
            "GIT_COMMITTER_NAME": "t",
            "GIT_COMMITTER_EMAIL": "t@t",
        },
    )
    _git(["-C", str(seed), "remote", "add", "origin", str(repo)], cwd=tmp_path)
    _git(["-C", str(seed), "push", "origin", "main"], cwd=tmp_path)
    return repo


def _commit_new_blob_upstream(upstream: Path, tmp_path: Path, *, path: str, content: str, ref: str = "main") -> str:
    """Add a fresh blob upstream and return the new commit SHA."""
    contrib = tmp_path / f"contrib-{path.replace('/', '_')}"
    _git(["clone", f"file://{upstream}", str(contrib)], cwd=tmp_path)
    (contrib / path).write_text(content, encoding="utf-8")
    _git(["-C", str(contrib), "add", path], cwd=tmp_path)
    subprocess.run(
        ["git", "-C", str(contrib), "commit", "-m", f"add {path}"],
        check=True,
        capture_output=True,
        text=True,
        env=os.environ
        | {
            "GIT_AUTHOR_NAME": "t",
            "GIT_AUTHOR_EMAIL": "t@t",
            "GIT_COMMITTER_NAME": "t",
            "GIT_COMMITTER_EMAIL": "t@t",
        },
    )
    sha = subprocess.run(
        ["git", "-C", str(contrib), "rev-parse", "HEAD"],
        check=True,
        capture_output=True,
        text=True,
    ).stdout.strip()
    _git(["-C", str(contrib), "push", "origin", f"HEAD:{ref}"], cwd=tmp_path)
    return sha


def _missing_object_oids(repo: Path, rev: str) -> list[str]:
    """OIDs of promisor-deferred objects reachable from ``rev``."""
    proc = subprocess.run(
        ["git", "-C", str(repo), "rev-list", "--objects", "--missing=print", rev],
        check=True,
        capture_output=True,
        text=True,
    )
    return [line[1:].split()[0] for line in proc.stdout.splitlines() if line.startswith("?")]


def test_fetch_ref_backfills_missing_blobs_into_partial_clone(tmp_path: Path) -> None:
    """Regression for oh-my-pi#1818: ``fetch_ref`` is called immediately before
    ``git worktree add origin/<ref>``. On a ``--filter=blob:none`` pool whose
    periodic ``fetch --prune`` inherited that filter, the ref's blobs are
    absent and the worktree-add triggers a promisor lazy fetch that — under
    proxy transport — has no PAT and dies. ``fetch_ref`` MUST materialize
    every reachable blob so the checkout never hits the lazy path."""
    upstream = _partial_clone_upstream(tmp_path)
    pool = tmp_path / "pool"
    _git(
        [
            "clone",
            "--filter=blob:none",
            "--no-tags",
            "--branch",
            "main",
            f"file://{upstream}",
            str(pool),
        ],
        cwd=tmp_path,
    )

    # New upstream commit → fresh blob not yet pulled into the pool.
    _commit_new_blob_upstream(upstream, tmp_path, path="payload.txt", content="v2 contents here\n")

    # Pool refresh mirrors `SandboxManager.ensure_clone` → inherits filter.
    git_fetch_prune(pool, token=None)
    missing_before = _missing_object_oids(pool, "origin/main")
    assert missing_before, (
        "test precondition broken: partial-clone fetch should leave at least one blob promisor-deferred"
    )

    # Pool config must show the partial-clone state we're recovering from.
    cfg_before = (pool / ".git" / "config").read_text(encoding="utf-8")
    assert "partialclonefilter = blob:none" in cfg_before
    assert "promisor = true" in cfg_before

    # The fix: fetch_ref backfills every reachable blob in a single call.
    git_fetch_ref(pool, "main", token=None)

    missing_after = _missing_object_oids(pool, "origin/main")
    assert missing_after == [], f"fetch_ref left missing objects: {missing_after}"

    # And the partial-clone config is intact — `fetch_prune` stays cheap on
    # the next pool refresh; only the explicit pre-checkout fetch eagerly
    # fills blobs.
    cfg_after = (pool / ".git" / "config").read_text(encoding="utf-8")
    assert "partialclonefilter = blob:none" in cfg_after
    assert "promisor = true" in cfg_after

    # End-to-end: worktree add must succeed even if origin is unreachable —
    # the blobs are local now, no lazy fetch can fire.
    _git(["-C", str(pool), "remote", "set-url", "origin", "https://example.invalid/missing.git"], cwd=tmp_path)
    ws_dir = tmp_path / "ws"
    subprocess.run(
        ["git", "-C", str(pool), "worktree", "add", "-b", "verify-1818", str(ws_dir), "origin/main"],
        check=True,
        capture_output=True,
        text=True,
        env=os.environ | {"GIT_TERMINAL_PROMPT": "0"},
    )
    assert (ws_dir / "payload.txt").read_text(encoding="utf-8") == "v2 contents here\n"


def test_fetch_pr_head_backfills_missing_blobs_into_partial_clone(tmp_path: Path) -> None:
    """Same regression as ``test_fetch_ref_backfills…`` but on the PR-review
    path: ``fetch_pr_head`` precedes ``git worktree add --detach FETCH_HEAD``
    and so MUST eagerly fetch blobs reachable from the PR head."""
    upstream = _partial_clone_upstream(tmp_path)
    pool = tmp_path / "pool"
    _git(
        [
            "clone",
            "--filter=blob:none",
            "--no-tags",
            "--branch",
            "main",
            f"file://{upstream}",
            str(pool),
        ],
        cwd=tmp_path,
    )

    # Publish a PR head with a fresh blob.
    pr_sha = _commit_new_blob_upstream(
        upstream, tmp_path, path="pr.txt", content="pr blob payload\n", ref="refs/pull/7/head"
    )

    git_fetch_pr_head(pool, 7, token=None)
    missing_after = _missing_object_oids(pool, pr_sha)
    assert missing_after == [], f"fetch_pr_head left missing objects: {missing_after}"

    # End-to-end: a detached worktree add against the freshly-fetched PR head
    # succeeds without lazy-fetching against (now-broken) origin.
    _git(["-C", str(pool), "remote", "set-url", "origin", "https://example.invalid/missing.git"], cwd=tmp_path)
    ws_dir = tmp_path / "pr-ws"
    subprocess.run(
        ["git", "-C", str(pool), "worktree", "add", "--detach", str(ws_dir), "FETCH_HEAD"],
        check=True,
        capture_output=True,
        text=True,
        env=os.environ | {"GIT_TERMINAL_PROMPT": "0"},
    )
    assert (ws_dir / "pr.txt").read_text(encoding="utf-8") == "pr blob payload\n"


# ---------------------------------------------------------------------------
# NativesCache integration into ensure_workspace
# ---------------------------------------------------------------------------


def _seed_native_dir(repo_dir: Path) -> Path:
    native_dir = repo_dir / "packages" / "natives" / "native"
    native_dir.mkdir(parents=True, exist_ok=True)
    return native_dir


def test_ensure_workspace_without_cache_leaves_native_dir_untouched(tmp_path: Path, upstream_repo: Path) -> None:
    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=10,
        title="no cache",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    assert mgr.natives_cache is None
    # No `packages/natives/native/` was tracked in the upstream, and no cache
    # is configured → the directory wasn't created by populate.
    assert not (ws.repo_dir / "packages" / "natives" / "native").exists()


def test_ensure_workspace_populates_from_natives_cache(tmp_path: Path, upstream_repo: Path) -> None:
    from robomp.natives_cache import NativesCache, compute_key, target_triple

    cache = NativesCache(tmp_path / "natives-cache")
    mgr = SandboxManager(tmp_path / "workspaces", natives_cache=cache)

    # First workspace: stage built artifacts, capture under the workspace's key.
    ws1 = mgr.ensure_workspace(
        repo="octo/widget",
        number=11,
        title="producer",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    native_dir1 = _seed_native_dir(ws1.repo_dir)
    # Mirror the napi build output set. The filename must match the live
    # `target_triple()` value or the populate path won't recognize it.
    triple = target_triple()
    (native_dir1 / f"pi_natives.{triple}.node").write_bytes(b"ELFx")
    (native_dir1 / "index.d.ts").write_text("export const X: number;\n")
    (native_dir1 / "index.js").write_text("export const X = 1;\n")
    (native_dir1 / "embedded-addon.js").write_text("export const embeddedAddon = null;\n")
    key = compute_key(ws1.repo_dir)  # default target = target_triple()
    assert cache.capture("octo/widget", key, native_dir1) is not None

    # Second workspace on the same source HEAD: ensure_workspace auto-populates.
    # We force the same key by pinning TARGET_VARIANT (only relevant on x64;
    # harmless on arm64) — actually compute_key uses target_triple() at call
    # time. To make the test platform-independent, override populate to use
    # the same key explicitly.
    ws2 = mgr.ensure_workspace(
        repo="octo/widget",
        number=12,
        title="consumer",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    native_dir2 = ws2.repo_dir / "packages" / "natives" / "native"
    # The auto-populate path used the real target_triple() — which matches
    # the host that just captured. So the same key applies and files appear.
    assert native_dir2.is_dir(), "populate should have created native/ on hit"
    node_name = f"pi_natives.{triple}.node"
    assert (native_dir2 / node_name).read_bytes() == b"ELFx"
    # The .node is hardlinked, sharing the cache's inode.
    cached_node = cache.entry_dir("octo/widget", key) / node_name
    ws2_node = native_dir2 / node_name
    assert cached_node.stat().st_ino == ws2_node.stat().st_ino


def test_ensure_workspace_cache_miss_is_silent_noop(tmp_path: Path, upstream_repo: Path) -> None:
    from robomp.natives_cache import NativesCache

    cache = NativesCache(tmp_path / "empty-cache")
    mgr = SandboxManager(tmp_path / "workspaces", natives_cache=cache)
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=13,
        title="miss",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    # Cache is empty so the workspace ends up identical to the no-cache case.
    assert ws.repo_dir.is_dir()
    assert not (ws.repo_dir / "packages" / "natives" / "native").exists()


def test_repo_lock_is_per_repo_identity(tmp_path: Path) -> None:
    mgr = SandboxManager(tmp_path)
    assert mgr._repo_lock("o/r") is mgr._repo_lock("o/r")
    assert mgr._repo_lock("o/r") is not mgr._repo_lock("o/r2")


def test_repo_lock_serializes_same_repo(tmp_path: Path) -> None:
    # Deterministic: while one thread holds the repo lock, a probe from a
    # DIFFERENT thread must fail to acquire the SAME repo's lock non-blockingly.
    mgr = SandboxManager(tmp_path)
    held = threading.Event()
    release = threading.Event()
    probe_result: dict[str, bool] = {}

    def holder() -> None:
        with mgr._repo_lock("o/r"):
            held.set()
            assert release.wait(2.0), "probe never completed"

    def probe() -> None:
        assert held.wait(2.0), "holder never acquired the lock"
        lock = mgr._repo_lock("o/r")
        got = lock.acquire(blocking=False)
        probe_result["acquired"] = got
        if got:
            lock.release()
        release.set()

    th = threading.Thread(target=holder)
    tp = threading.Thread(target=probe)
    th.start()
    tp.start()
    th.join(3.0)
    tp.join(3.0)
    # Same repo, held cross-thread -> non-blocking acquire MUST fail.
    assert probe_result.get("acquired") is False, (
        "same-repo lock was acquirable from another thread while held (did not serialize)"
    )


def test_repo_lock_allows_distinct_repos_to_overlap(tmp_path: Path) -> None:
    # Deterministic: holding one repo's lock must NOT block acquiring a
    # DIFFERENT repo's lock from another thread.
    mgr = SandboxManager(tmp_path)
    held = threading.Event()
    release = threading.Event()
    probe_result: dict[str, bool] = {}

    def holder() -> None:
        with mgr._repo_lock("o/a"):
            held.set()
            assert release.wait(2.0), "probe never completed"

    def probe() -> None:
        assert held.wait(2.0), "holder never acquired the lock"
        lock = mgr._repo_lock("o/b")
        got = lock.acquire(blocking=False)
        probe_result["acquired"] = got
        if got:
            lock.release()
        release.set()

    th = threading.Thread(target=holder)
    tp = threading.Thread(target=probe)
    th.start()
    tp.start()
    th.join(3.0)
    tp.join(3.0)
    # Distinct repos -> the other lock is free -> non-blocking acquire succeeds.
    assert probe_result.get("acquired") is True, (
        "distinct-repo lock was NOT acquirable while an unrelated repo's lock was held"
    )


def test_ensure_workspace_acquires_repo_lock(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    mgr = SandboxManager(tmp_path)
    mgr.natives_cache = None
    real = threading.RLock()
    events: list[str | tuple[str, str]] = []

    class Rec:
        def __enter__(self) -> Rec:
            events.append("acquire")
            real.acquire()
            return self

        def __exit__(self, *a: object) -> bool:
            real.release()
            events.append("release")
            return False

    def fake_lock(repo: str) -> Rec:
        events.append(("lock", repo))
        return Rec()

    monkeypatch.setattr(mgr, "_repo_lock", fake_lock)
    mgr.transport = SimpleNamespace(
        clone_pool=lambda **k: None,
        fetch_pool=lambda **k: None,
        fetch_base_ref=lambda **k: None,
        fetch_pr_head=lambda **k: None,
    )  # type: ignore
    ok = subprocess.CompletedProcess(["x"], 0, "", "")
    monkeypatch.setattr("robomp.sandbox._run", lambda *a, **k: ok)
    monkeypatch.setattr("robomp.sandbox._safe_run", lambda *a, **k: ok)
    monkeypatch.setattr("robomp.sandbox._chown_workspace", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._provision_runtime_dirs", lambda *a, **k: None)

    ws = mgr.ensure_workspace(
        repo="o/r",
        number=1,
        title="t",
        clone_url="https://x/o/r.git",
        default_branch="main",
        author_name="n",
        author_email="e@e",
        slot_uid=None,
    )
    assert ("lock", "o/r") in events
    assert events.count("acquire") == 1
    assert events.count("release") == 1
    assert ws.repo_full_name == "o/r"


def test_remove_workspace_acquires_repo_lock(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    mgr = SandboxManager(tmp_path)
    events: list[str | tuple[str, str]] = []
    real = threading.RLock()

    class Rec:
        def __enter__(self) -> Rec:
            events.append("acquire")
            real.acquire()
            return self

        def __exit__(self, *a: object) -> bool:
            real.release()
            events.append("release")
            return False

    monkeypatch.setattr(mgr, "_repo_lock", lambda repo: (events.append(("lock", repo)), Rec())[1])
    # ws_root does not exist -> remove_workspace just no-ops inside the lock
    mgr.remove_workspace(repo="o/r", number=99)
    assert ("lock", "o/r") in events
    assert events.count("acquire") == 1 and events.count("release") == 1


def test_safe_run_timeout_returns_124(monkeypatch: pytest.MonkeyPatch) -> None:
    import robomp.sandbox as s

    seen: dict[str, object] = {}

    def boom(*a: object, **k: object) -> subprocess.CompletedProcess:
        seen["timeout"] = k.get("timeout")
        cmd = a[0] if a else k.get("args", ["git"])
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=1)  # type: ignore

    monkeypatch.setattr("robomp.sandbox.subprocess.run", boom)
    r = s._safe_run(["git", "status"])
    assert r.returncode == 124
    assert seen["timeout"] == s._DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT


def test_run_timeout_raises_git_command_error_124(monkeypatch: pytest.MonkeyPatch) -> None:
    import robomp.sandbox as s

    seen: dict[str, object] = {}

    def boom(*a: object, **k: object) -> subprocess.CompletedProcess:
        seen["timeout"] = k.get("timeout")
        cmd = a[0] if a else k.get("args", ["git"])
        raise subprocess.TimeoutExpired(cmd=cmd, timeout=1)  # type: ignore

    monkeypatch.setattr("robomp.sandbox.subprocess.run", boom)
    with pytest.raises(s.GitCommandError) as exc:
        s._run(["git", "status"])
    assert exc.value.returncode == 124
    assert seen["timeout"] == s._DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT


def test_ensure_workspace_raises_when_local_branch_probe_times_out(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    mgr = SandboxManager(tmp_path)
    mgr.natives_cache = None
    mgr.transport = SimpleNamespace(
        clone_pool=lambda **k: None,
        fetch_pool=lambda **k: None,
        fetch_base_ref=lambda **k: None,
        fetch_pr_head=lambda **k: None,
    )  # type: ignore

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        if "rev-parse" in cmd and cmd[-1].startswith("refs/heads/"):
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)
    monkeypatch.setattr("robomp.sandbox._run", lambda *a, **k: subprocess.CompletedProcess(["x"], 0, "", ""))
    monkeypatch.setattr("robomp.sandbox._chown_workspace", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._provision_runtime_dirs", lambda *a, **k: None)

    with pytest.raises(GitCommandError):
        mgr.ensure_workspace(
            repo="o/r",
            number=1,
            title="t",
            clone_url="https://x/o/r.git",
            default_branch="main",
            author_name="n",
            author_email="e@e",
            existing_branch="feature/x",
            slot_uid=None,
        )


def test_ensure_workspace_raises_when_remote_branch_probe_times_out(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    mgr = SandboxManager(tmp_path)
    mgr.natives_cache = None
    mgr.transport = SimpleNamespace(
        clone_pool=lambda **k: None,
        fetch_pool=lambda **k: None,
        fetch_base_ref=lambda **k: None,
        fetch_pr_head=lambda **k: None,
    )  # type: ignore

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        if "rev-parse" in cmd and cmd[-1].startswith("refs/heads/"):
            return subprocess.CompletedProcess(cmd, 128, "", "")
        if "rev-parse" in cmd and cmd[-1].startswith("refs/remotes/origin/"):
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)
    monkeypatch.setattr("robomp.sandbox._run", lambda *a, **k: subprocess.CompletedProcess(["x"], 0, "", ""))
    monkeypatch.setattr("robomp.sandbox._chown_workspace", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._provision_runtime_dirs", lambda *a, **k: None)

    with pytest.raises(GitCommandError):
        mgr.ensure_workspace(
            repo="o/r",
            number=1,
            title="t",
            clone_url="https://x/o/r.git",
            default_branch="main",
            author_name="n",
            author_email="e@e",
            existing_branch="feature/x",
            slot_uid=None,
        )


def test_ensure_workspace_raises_when_symbolic_ref_probe_times_out(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    mgr = SandboxManager(tmp_path)
    mgr.natives_cache = None
    mgr.transport = SimpleNamespace(
        clone_pool=lambda **k: None,
        fetch_pool=lambda **k: None,
        fetch_base_ref=lambda **k: None,
        fetch_pr_head=lambda **k: None,
    )  # type: ignore

    # Force the repo_exists=True branch: the worktree already has a .git.
    repo_dir = mgr.workspace_root("o/r", 1) / "repo"
    (repo_dir / ".git").mkdir(parents=True)

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        if cmd[:2] == ["git", "symbolic-ref"]:
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)
    monkeypatch.setattr("robomp.sandbox._run", lambda *a, **k: subprocess.CompletedProcess(["x"], 0, "", ""))
    monkeypatch.setattr("robomp.sandbox._chown_workspace", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._provision_runtime_dirs", lambda *a, **k: None)
    monkeypatch.setattr("robomp.sandbox._git_env_for_repo", lambda *a, **k: {})

    with pytest.raises(GitCommandError):
        mgr.ensure_workspace(
            repo="o/r",
            number=1,
            title="t",
            clone_url="https://x/o/r.git",
            default_branch="main",
            author_name="n",
            author_email="e@e",
            existing_branch="feature/x",
            slot_uid=None,
        )


def test_remove_workspace_real_prune_clears_dangling_registration(
    tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Integration guard: the other prune tests fully mock `_safe_run`, so the
    # REAL `git worktree prune` never runs and the stale-metadata risk this
    # patch targets is not actually exercised. Here we build a real workspace
    # (real pool clone + real worktree registration), fake ONLY the
    # `git worktree remove` to "time out" (124), and let the real rmtree+prune
    # run. The dangling registration must actually be gone afterward, and a
    # fresh add at the same path must succeed — the real integration risk.
    import robomp.sandbox as s

    mgr = SandboxManager(tmp_path / "workspaces")
    ws = mgr.ensure_workspace(
        repo="octo/widget",
        number=21,
        title="t",
        clone_url=str(upstream_repo),
        default_branch="main",
        author_name="robomp-bot",
        author_email="robomp-bot@example.invalid",
    )
    pool = mgr.pool_path("octo/widget")
    repo_dir = ws.repo_dir
    assert repo_dir.exists()
    listed = subprocess.run(
        ["git", "-C", str(pool), "worktree", "list", "--porcelain"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout
    assert str(repo_dir) in listed  # sanity: registration exists

    real_safe_run = s._safe_run

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        if cmd[:3] == ["git", "worktree", "remove"]:
            # A remove that "times out" without unregistering — the source must
            # then rmtree the checkout and run the REAL prune to clear metadata.
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return real_safe_run(cmd, **k)  # type: ignore[arg-type]

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    mgr.remove_workspace(repo="octo/widget", number=21)

    listed_after = subprocess.run(
        ["git", "-C", str(pool), "worktree", "list", "--porcelain"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout
    assert str(repo_dir) not in listed_after, listed_after
    # The path is genuinely reusable again — the actual failure the fix prevents.
    subprocess.run(
        ["git", "-C", str(pool), "worktree", "add", "--detach", str(repo_dir), "HEAD"],
        capture_output=True,
        text=True,
        check=True,
    )
    assert repo_dir.exists()


def test_worktree_add_cleans_partial_state_on_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    # A `git worktree add` killed mid-op (124) or otherwise failing must leave no
    # partial checkout and no dangling registration, or the event retry poisons
    # itself on the same path.
    import robomp.sandbox as s

    pool = tmp_path / "pool"
    pool.mkdir()
    repo_dir = tmp_path / "ws" / "repo"
    repo_dir.mkdir(parents=True)
    (repo_dir / "leftover").write_text("partial", encoding="utf-8")

    def boom_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        raise s.GitCommandError(cmd, 124, "", "timed out")

    pruned: list[list[str]] = []

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        pruned.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._run", boom_run)
    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    with pytest.raises(GitCommandError) as exc:
        s._worktree_add(["git", "worktree", "add", str(repo_dir), "main"], pool=pool, repo_dir=repo_dir)

    assert exc.value.returncode == 124  # the original add failure is surfaced
    assert not repo_dir.exists()  # partial checkout removed
    assert ["git", "worktree", "prune"] in pruned  # pool pruned


def test_worktree_add_raises_prune_failure_chained_from_add_error(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # If the cleanup prune ALSO fails, that failure must be raised (a dangling
    # registration left behind is what poisons the retry) — chained from the
    # original add error so the root cause is not lost.
    import robomp.sandbox as s

    pool = tmp_path / "pool"
    pool.mkdir()
    repo_dir = tmp_path / "ws" / "repo"
    repo_dir.mkdir(parents=True)

    add_err = s.GitCommandError(["git", "worktree", "add"], 1, "", "add failed")

    def boom_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        raise add_err

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        return subprocess.CompletedProcess(cmd, 124, "", "prune timed out")

    monkeypatch.setattr("robomp.sandbox._run", boom_run)
    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    with pytest.raises(GitCommandError) as exc:
        s._worktree_add(["git", "worktree", "add", str(repo_dir), "main"], pool=pool, repo_dir=repo_dir)

    assert exc.value.returncode == 124  # the PRUNE failure (124), not the add (1)
    assert exc.value.__cause__ is add_err  # chained from the add error


def test_ensure_clone_fails_before_fetch_when_origin_probe_times_out(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # An older deploy may have baked `https://user:pass@…` into origin;
    # `_reset_origin_url` rewrites it before fetch so the PAT never persists. A
    # timed-out `git remote get-url origin` (124) is indeterminate — ensure_clone
    # must fail closed BEFORE fetch_pool rather than fetch against a possibly-
    # credentialed origin.
    mgr = SandboxManager(tmp_path / "workspaces")
    pool = mgr.pool_path("octo/widget")
    (pool / ".git").mkdir(parents=True)  # take the idempotent-refresh path

    fetched = {"called": False}
    mgr.transport = SimpleNamespace(
        fetch_pool=lambda **k: fetched.__setitem__("called", True),
        clone_pool=lambda **k: None,
    )  # type: ignore

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        if cmd[:4] == ["git", "remote", "get-url", "origin"]:
            return subprocess.CompletedProcess(cmd, 124, "", "timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    with pytest.raises(GitCommandError):
        mgr.ensure_clone(repo="octo/widget", clone_url="https://github.com/octo/widget.git", default_branch="main")
    assert fetched["called"] is False, (
        "fetch_pool ran despite an indeterminate origin probe — a legacy credential could persist and be reused"
    )


def test_remove_workspace_raises_when_prune_times_out(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    # After a failed `git worktree remove`, `git worktree prune` is the step that
    # clears the dangling registration. If prune ITSELF fails (incl. a 124
    # timeout), remove_workspace must raise so the cleanup event retries — not
    # record success while stale metadata still blocks the next add.
    mgr = SandboxManager(tmp_path)
    ws_root = mgr.workspace_root("o/r", 31)
    repo_dir = ws_root / "repo"
    repo_dir.mkdir(parents=True, exist_ok=True)
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)
    (pool / ".git").mkdir(exist_ok=True)  # mark as a real git pool for the cleanup gate

    calls: list[list[str]] = []

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        calls.append(list(cmd))
        if cmd[:3] == ["git", "worktree", "remove"]:
            # Distinct code (1, NOT 124) so the assertion below proves the raised
            # error came from PRUNE, not from the remove failure.
            return subprocess.CompletedProcess(cmd, 1, "", "remove failed")
        if cmd[:3] == ["git", "worktree", "prune"]:
            return subprocess.CompletedProcess(cmd, 124, "", "prune timed out")
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    with pytest.raises(GitCommandError) as exc:
        mgr.remove_workspace(repo="o/r", number=31)
    # Prune actually ran, and the surfaced error is prune's (124) — not remove's (1).
    assert ["git", "worktree", "prune"] in calls, "prune did not run after a failed remove"
    assert exc.value.returncode == 124


def test_remove_workspace_prunes_when_checkout_already_gone_on_entry(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # A prior remove/worktree-add killed mid-flight can leave the checkout gone
    # but the pool's worktree registration dangling. On the NEXT remove_workspace
    # the checkout is already missing on entry, yet the stale registration must
    # still be pruned — the old `if repo_dir.exists()` guard skipped it entirely.
    mgr = SandboxManager(tmp_path)
    ws_root = mgr.workspace_root("o/r", 33)
    repo_dir = ws_root / "repo"
    ws_root.mkdir(parents=True, exist_ok=True)  # ws_root present, but NO repo_dir
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)
    (pool / ".git").mkdir(exist_ok=True)  # mark as a real git pool for the cleanup gate
    assert not repo_dir.exists()  # precondition: checkout already gone

    calls: list[list[str]] = []

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    mgr.remove_workspace(repo="o/r", number=33)

    # No `git worktree remove` (nothing to remove), but prune MUST run to clear
    # any dangling registration for the missing path.
    assert ["git", "worktree", "prune"] in calls, (
        "prune was skipped for a checkout already gone on entry — dangling registration would persist"
    )
    assert not any(c[:3] == ["git", "worktree", "remove"] for c in calls)
    assert not ws_root.exists()  # ws_root still cleaned up


def test_remove_workspace_no_git_ops_when_pool_is_not_a_real_clone(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # `ensure_clone` mkdir's the pool dir BEFORE cloning, so a failed first clone
    # can leave a non-git dir at pool_path. A later remove_workspace (e.g. on
    # reopen) must NOT run `git worktree prune` there — it would error on a
    # non-git dir and raise. It must be a clean no-op that still clears ws_root.
    mgr = SandboxManager(tmp_path)
    ws_root = mgr.workspace_root("o/r", 41)
    ws_root.mkdir(parents=True, exist_ok=True)
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)  # exists but is NOT a git repo (no .git/HEAD)

    calls: list[list[str]] = []

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 128, "", "not a git repository")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    # Must NOT raise despite the pool dir existing.
    mgr.remove_workspace(repo="o/r", number=41)

    assert calls == [], "ran git in a non-git pool dir — would error and raise on cleanup"
    assert not ws_root.exists()  # ws_root still cleaned up


def test_remove_workspace_skips_prune_on_repeat_close_after_full_cleanup(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # A fully-cleaned workspace has no ws_root. A duplicate/repeat close (GitHub
    # can redeliver a close webhook) must NOT run a speculative `git worktree
    # prune` on the shared pool every time — there is no dangling registration
    # once the first cleanup succeeded. Guarded by `ws_root.exists()`.
    mgr = SandboxManager(tmp_path)
    ws_root = mgr.workspace_root("o/r", 43)
    repo_dir = ws_root / "repo"
    pool = mgr.pool_path("o/r")
    pool.mkdir(parents=True, exist_ok=True)
    (pool / ".git").mkdir(exist_ok=True)  # a REAL git pool (persists across closes)
    assert not ws_root.exists() and not repo_dir.exists()  # already fully cleaned

    calls: list[list[str]] = []

    def fake_safe_run(cmd: list[str], **k: object) -> subprocess.CompletedProcess[str]:
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0, "", "")

    monkeypatch.setattr("robomp.sandbox._safe_run", fake_safe_run)

    mgr.remove_workspace(repo="o/r", number=43)

    assert calls == [], "repeat close ran a spurious git worktree prune on an already-clean workspace"
