use crate::debug_log;
use crate::docker::CommandRunner;
use crate::isolation::MountIsolation;
use crate::isolation::branch::branch_name;
use crate::isolation::state::{CleanupStatus, IsolationRecord, read_record, upsert_record};
use crate::workspace::ResolvedWorkspace;
use anyhow::Context;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterializedWorkspace {
    pub workdir: String,
    pub mounts: Vec<MaterializedMount>,
    /// Threaded through from `ResolvedWorkspace` so `launch_role_runtime`
    /// can stamp the `jackin.keep_awake=true` label on the container
    /// without a config re-lookup. Read by the keep-awake reconciler.
    pub keep_awake_enabled: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterializedMount {
    pub bind_src: String,
    pub dst: String,
    pub readonly: bool,
    pub isolation: MountIsolation,
    /// Auxiliary docker bind mounts required so git can operate on this
    /// worktree from inside the container. `None` for `Shared` mounts.
    /// See the per-mount-isolation roadmap entry "Container-side mount
    /// layout" and "Design Decision: Worktree Materialization Layout"
    /// for the rationale and topology.
    pub worktree_aux: Option<WorktreeAuxMounts>,
}

/// Three extra bind mounts the container needs so a worktree's gitdir
/// relationship resolves consistently inside the container.
///
/// Single top-level container path: everything jackin contributes lives
/// under `/jackin/host/<dst-stripped>/.git/`. The host repo's full
/// `.git/` is bind-mounted there (rw); the admin dir for this
/// worktree is at `worktrees/<container>/` natively (part of the same
/// mount). The two override files are file-level overlays inside that
/// directory mount: one shadowing the worktree's `.git` text file (so
/// the role's gitdir resolves to a container path), one shadowing the
/// admin's `gitdir` back-pointer (so git's integrity check passes
/// where `<dst>/.git` differs from the host worktree path). All
/// sources are either jackin-owned (override files under the container
/// state dir) or host-owned via bind mount; host worktree files are
/// never modified.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeAuxMounts {
    /// Host source: `<host_repo>/.git`. Container target:
    /// `/jackin/host/<dst-stripped>/.git`. Read-write — git writes
    /// refs, objects, HEAD/index/logs all under here on every
    /// commit/branch/fetch. The destination intentionally mirrors the
    /// host topology so that `docker inspect` shows symmetric
    /// Source/Destination paths both ending in `.git`. Includes the
    /// per-worktree admin dir at `worktrees/<container>/` natively —
    /// no separate admin mount is needed; the on-disk `commondir`
    /// (`../..`) resolves correctly inside this container path.
    pub host_git_dir: String,
    pub host_git_target: String,
    /// Host source: jackin-owned override `.git` file containing
    /// `gitdir: /jackin/host/<dst-stripped>/.git/worktrees/<container>`.
    /// Container target: `<dst>/.git`. Redirects the worktree's gitdir
    /// to the admin entry inside the host `.git/` mount.
    pub git_file_override: String,
    pub git_file_target: String,
    /// Host source: jackin-owned override file containing `<dst>/.git`.
    /// Container target:
    /// `/jackin/host/<dst-stripped>/.git/worktrees/<container>/gitdir`.
    /// Overrides git's admin-dir back-pointer so its verification check
    /// (back-pointer must match the worktree's `.git` location) passes
    /// inside the container, where `<dst>` differs from the host
    /// worktree path. This is a file-level overlay on top of a
    /// directory-level mount (the host `.git/` mount) — Docker handles
    /// this natively by shadowing only the single file.
    pub gitdir_back_override: String,
    pub gitdir_back_target: String,
}

/// Compute the host-side worktree path for an isolated mount.
///
/// The path's *basename* matters: it's what `git worktree add` uses
/// as the admin entry name in `<host_repo>/.git/worktrees/<n>/`. We
/// use the container name as the basename so admin entries are
/// globally unique per (`host_repo`, container) — `git worktree list`
/// on the host shows which container owns each worktree at a glance.
/// No auto-suffix needed; no read-back required.
/// `validate_isolation_layout` already rejects two isolated mounts on
/// the same host repo within one workspace, so the basename can never
/// collide with itself; cross-container collisions are avoided by
/// container-name uniqueness.
///
/// On-disk layout (groups all git-related artifacts under
/// `<container_state>/git/`, with `worktree/repo/` marking the subtree
/// as git-managed and `overrides/` holding jackin's bind-mount sources):
///
/// ```text
/// <container_state>/git/
/// ├── worktree/repo/<dst-stripped>/<container>/   ← THIS path (the materialized git worktree)
/// └── overrides/<dst-stripped>/                    ← jackin-owned override files (see write_git_overrides)
///     ├── .git
///     └── gitdir
/// ```
pub fn worktree_path_for(container_state_dir: &Path, dst: &str, container_name: &str) -> PathBuf {
    let rel = dst.trim_matches('/');
    container_state_dir
        .join("git")
        .join("worktree")
        .join("repo")
        .join(rel)
        .join(container_name)
}

/// Compute the host-side clone path for an isolated mount.
///
/// Clone mode stores a full per-agent repository under the same
/// per-container `git/` tree as worktree mode, but uses a separate
/// top-level discriminator so cleanup and manual inspection are obvious:
///
/// ```text
/// <container_state>/git/clone/repo/<dst-stripped>/<container>/
/// ```
pub fn clone_path_for(container_state_dir: &Path, dst: &str, container_name: &str) -> PathBuf {
    let rel = dst.trim_matches('/');
    container_state_dir
        .join("git")
        .join("clone")
        .join("repo")
        .join(rel)
        .join(container_name)
}

/// Container-side path where the host repo's `.git/` is bind-mounted.
/// Mirrors the host topology under `/jackin/host/` so:
///
///   docker inspect Source       = `<host_repo>/.git`
///   docker inspect Destination  = `/jackin/host/<dst-stripped>/.git`
///
/// reads symmetrically — both ends terminate in `.git`. Per-mount
/// disambiguation comes from `<dst-stripped>` (= `dst.trim_matches('/')`,
/// slashes preserved as directory separators), matching the scheme
/// `worktree_path_for` already uses for the worktree itself.
fn container_host_git_path(mount_dst: &str) -> String {
    let rel = mount_dst.trim_matches('/');
    format!("/jackin/host/{rel}/.git")
}

/// Drop an embedded `userinfo@` from an HTTP(S) URL so a host-side
/// PAT in the operator's `origin` does not get copied into the
/// per-container clone's `.git/config`. SCP / `ssh://` forms pass
/// through unchanged — leading `git@` is an SSH identity, not a
/// credential.
fn strip_userinfo(url: String) -> String {
    for scheme in ["https://", "http://"] {
        if let Some(rest) = url.strip_prefix(scheme) {
            let (authority, path) = rest.split_once('/').unwrap_or((rest, ""));
            if let Some((_userinfo, host)) = authority.rsplit_once('@') {
                return if path.is_empty() {
                    format!("{scheme}{host}")
                } else {
                    format!("{scheme}{host}/{path}")
                };
            }
            return url;
        }
    }
    url
}

/// Write the two jackin-owned override files alongside the materialized
/// worktree. Idempotent: rewrites both files on every call so reused
/// worktrees pick up any topology changes (rare, but cheap).
///
/// Storage layout: every git-related artifact for one mount lives
/// under `<container_state>/git/`, with `worktree/repo/` marking the
/// subtree as git-managed and `overrides/` holding jackin's
/// bind-mount sources. Override-file names match their docker mount
/// destinations:
///
/// ```text
/// <container_state>/git/
/// ├── worktree/repo/<dst-stripped>/<container>/  (materialized by `git worktree add`; see worktree_path_for)
/// └── overrides/<dst-stripped>/
///     ├── .git    → mounted at <dst>/.git (`:ro`)
///     └── gitdir  → mounted at /jackin/host/<dst-stripped>/.git/worktrees/<container>/gitdir (`:ro`)
/// ```
///
/// `.git` redirects gitdir to the admin entry inside the host `.git/`
/// mount (`/jackin/host/<dst-stripped>/.git/worktrees/<container>`).
/// `gitdir` is the back-pointer matching the worktree's `.git`
/// location inside the container (`<dst>/.git`).
///
/// No `commondir` override is needed: with the admin dir living
/// natively inside the host `.git/` mount at `worktrees/<container>/`,
/// the on-disk default `commondir = ../..` resolves correctly inside
/// the container (to the shared `.git/`).
///
/// Returns the [`WorktreeAuxMounts`] needed to wire up the three
/// auxiliary bind mounts at docker-run time.
fn write_git_overrides(
    container_state_dir: &Path,
    mount_dst: &str,
    container_name: &str,
    host_repo_src: &str,
) -> anyhow::Result<WorktreeAuxMounts> {
    let rel = mount_dst.trim_matches('/');
    let mount_overrides_dir = container_state_dir.join("git").join("overrides").join(rel);
    std::fs::create_dir_all(&mount_overrides_dir)
        .with_context(|| format!("create overrides dir at {}", mount_overrides_dir.display()))?;

    let host_git_target = container_host_git_path(mount_dst);

    // Override 1 (`.git`): replacement worktree pointer file. Mounted
    // at `<dst>/.git` inside the container. Redirects gitdir to the
    // admin entry inside the host `.git/` mount at
    // `worktrees/<container>/`. Admin name = container name
    // (deterministic — validation rejects same-host-repo siblings,
    // and container names are globally unique, so no auto-suffix or
    // read-back is required).
    let git_file_override_path = mount_overrides_dir.join(".git");
    let git_file_content = format!("gitdir: {host_git_target}/worktrees/{container_name}\n");
    std::fs::write(&git_file_override_path, &git_file_content)
        .with_context(|| format!("write .git override {}", git_file_override_path.display()))?;

    // Override 2 (`gitdir`): replacement back-pointer mounted at
    // `<host_git_target>/worktrees/<container>/gitdir`. Tells git
    // "the worktree's `.git` file is at `<dst>/.git`" so its
    // verification check passes (the host's absolute path stored in
    // the on-disk back-pointer would NOT match `<dst>` inside the
    // container, hence the override).
    let gitdir_back_override_path = mount_overrides_dir.join("gitdir");
    let gitdir_back_content = format!("{mount_dst}/.git\n");
    std::fs::write(&gitdir_back_override_path, &gitdir_back_content).with_context(|| {
        format!(
            "write gitdir override {}",
            gitdir_back_override_path.display()
        )
    })?;

    let host_git_dir = format!("{host_repo_src}/.git");
    let git_file_target = format!("{mount_dst}/.git");
    let gitdir_back_target = format!("{host_git_target}/worktrees/{container_name}/gitdir");

    Ok(WorktreeAuxMounts {
        host_git_dir,
        host_git_target,
        git_file_override: git_file_override_path.to_string_lossy().into(),
        git_file_target,
        gitdir_back_override: gitdir_back_override_path.to_string_lossy().into(),
        gitdir_back_target,
    })
}

/// Enable `extensions.worktreeConfig` on a host repo if not already set.
/// Returns Ok(true) when newly enabled (caller may print a notice),
/// Ok(false) when already enabled.
pub async fn ensure_worktree_config_enabled(
    repo: &Path,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<bool> {
    let current = runner
        .capture(
            "git",
            &[
                "-C",
                &repo.to_string_lossy(),
                "config",
                "--get",
                "extensions.worktreeConfig",
            ],
            None,
        )
        .await
        .unwrap_or_default();
    if current.trim() == "true" {
        debug_log!(
            "isolation",
            "extensions.worktreeConfig already enabled at {}",
            repo.display()
        );
        return Ok(false);
    }
    let format_version = runner
        .capture(
            "git",
            &[
                "-C",
                &repo.to_string_lossy(),
                "config",
                "--get",
                "core.repositoryformatversion",
            ],
            None,
        )
        .await
        .unwrap_or_default();
    if format_version.trim() == "0" || format_version.trim().is_empty() {
        debug_log!(
            "isolation",
            "bumping core.repositoryformatversion 0 -> 1 at {} (required for extensions.worktreeConfig)",
            repo.display()
        );
        runner
            .run(
                "git",
                &[
                    "-C",
                    &repo.to_string_lossy(),
                    "config",
                    "core.repositoryformatversion",
                    "1",
                ],
                None,
                &crate::docker::RunOptions::default(),
            )
            .await?;
    }
    debug_log!(
        "isolation",
        "enabling extensions.worktreeConfig at {} (per-worktree config from now on)",
        repo.display()
    );
    runner
        .run(
            "git",
            &[
                "-C",
                &repo.to_string_lossy(),
                "config",
                "extensions.worktreeConfig",
                "true",
            ],
            None,
            &crate::docker::RunOptions::default(),
        )
        .await?;
    Ok(true)
}

/// Filesystem probe (loose ref then `packed-refs`) rather than `git
/// show-ref` to keep the test `CommandRunner` capture queue stable.
fn find_local_branch_tip(repo: &str, branch: &str) -> Option<String> {
    let git_dir = std::path::Path::new(repo).join(".git");
    let mut loose = git_dir.join("refs").join("heads");
    for segment in branch.split('/') {
        loose = loose.join(segment);
    }
    match std::fs::read_to_string(&loose) {
        Ok(contents) => {
            let sha = contents.trim();
            // Symref content (`ref: refs/heads/foo`) and a 0-byte
            // ref file are pathological local states that would
            // otherwise be returned verbatim and poison the
            // `IsolationRecord.base_commit` SHA field. Fall through
            // to packed-refs and (failing that) `None` so the caller
            // takes the fresh `-b` path; git will surface its own
            // error if the branch genuinely does exist somewhere
            // unreadable.
            if sha.is_empty() {
                debug_log!(
                    "isolation",
                    "find_local_branch_tip: loose ref {loose} present but empty — treating as missing",
                    loose = loose.display(),
                );
            } else if sha.starts_with("ref:") {
                debug_log!(
                    "isolation",
                    "find_local_branch_tip: loose ref {loose} is a symref ({sha}) — treating as missing",
                    loose = loose.display(),
                );
            } else {
                return Some(sha.to_string());
            }
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
        Err(e) => {
            // Permission / EIO / non-UTF-8: surface so --debug
            // correlates with the later worktree-add failure.
            debug_log!(
                "isolation",
                "find_local_branch_tip: read {loose} failed: {e}",
                loose = loose.display(),
            );
        }
    }
    let packed = git_dir.join("packed-refs");
    let contents = match std::fs::read_to_string(&packed) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
        Err(e) => {
            debug_log!(
                "isolation",
                "find_local_branch_tip: read {packed} failed: {e}",
                packed = packed.display(),
            );
            return None;
        }
    };
    let want = format!("refs/heads/{branch}");
    for line in contents.lines() {
        if line.starts_with('#') || line.starts_with('^') {
            continue;
        }
        let Some((sha, refname)) = line.split_once(char::is_whitespace) else {
            continue;
        };
        if refname.trim() == want {
            return Some(sha.trim().to_string());
        }
    }
    None
}

use crate::workspace::MountConfig;

#[derive(Debug, Clone)]
pub struct PreflightContext {
    pub workspace_name: String,
    pub force: bool,
    pub interactive: bool,
}

/// Validation that must pass before host-side git materialization. Layout
/// validation (parent/child rejection) happens earlier at config-validation
/// time; this is per-mount.
pub async fn preflight_isolated(
    mount: &MountConfig,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<()> {
    // readonly is incompatible with isolated editable source modes.
    anyhow::ensure!(
        !mount.readonly,
        "isolated mount `{}` cannot be readonly (isolation = {})",
        mount.dst,
        mount.isolation
    );

    // Sensitive mount overlap.
    let sensitives = crate::workspace::find_sensitive_mounts(std::slice::from_ref(mount));
    if let Some(s) = sensitives.first() {
        anyhow::bail!(
            "isolated mount `{}` overlaps sensitive path `{}` ({}) (isolation = {})",
            mount.dst,
            s.src,
            s.reason,
            mount.isolation,
        );
    }

    let src = std::path::Path::new(&mount.src);

    // Mid-rebase / merge / cherry-pick guard.
    for marker in &[
        "rebase-merge",
        "rebase-apply",
        "MERGE_HEAD",
        "CHERRY_PICK_HEAD",
    ] {
        if src.join(".git").join(marker).exists() {
            anyhow::bail!(
                "isolated mount `{}`: host repo `{}` is mid-{}; resolve before launching",
                mount.dst,
                mount.src,
                marker
            );
        }
    }

    // src must be a git repo *root* — toplevel must equal src.
    let toplevel = runner
        .capture(
            "git",
            &["-C", &mount.src, "rev-parse", "--show-toplevel"],
            None,
        )
        .await
        .with_context(|| {
            format!(
                "isolated mount `{}`: git rev-parse --show-toplevel",
                mount.dst
            )
        })?;
    let toplevel = toplevel.trim();
    let src_canon =
        std::fs::canonicalize(src).with_context(|| format!("canonicalize {}", mount.src))?;
    let top_canon =
        std::fs::canonicalize(toplevel).with_context(|| format!("canonicalize {toplevel}"))?;
    anyhow::ensure!(
        src_canon == top_canon,
        "isolated mount `{}`: src `{}` is inside repo `{}` but not its root",
        mount.dst,
        mount.src,
        toplevel
    );

    // Dirty tree check (separate test in 4.5).
    check_dirty_tree(mount, ctx, runner).await?;

    Ok(())
}

pub async fn preflight_worktree(
    mount: &MountConfig,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<()> {
    preflight_isolated(mount, ctx, runner).await
}

async fn check_dirty_tree(
    mount: &MountConfig,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<()> {
    let porcelain = runner
        .capture("git", &["-C", &mount.src, "status", "--porcelain"], None)
        .await
        .with_context(|| format!("isolated mount `{}`: git status --porcelain", mount.dst))?;
    if porcelain.trim().is_empty() {
        return Ok(());
    }
    if ctx.force {
        eprintln!(
            "[jackin] proceeding with dirty host tree at `{}` (--force)",
            mount.src
        );
        return Ok(());
    }
    if ctx.interactive {
        return Ok(());
    }
    anyhow::bail!(
        "isolated mount `{}`: host tree at `{}` is dirty (staged/unstaged/untracked); \
         pass --force to acknowledge, or commit/stash before launching",
        mount.dst,
        mount.src
    );
}

/// Top-level materialization.
///
/// Iterates the resolved workspace mounts, passes through `Shared` mounts,
/// and per-mount-materializes `Worktree` mounts. Returns the
/// `MaterializedWorkspace` ready for Docker launch.
pub async fn materialize_workspace(
    resolved: &ResolvedWorkspace,
    container_state_dir: &Path,
    selector_key: &str,
    container_name: &str,
    workspace_name: &str,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<MaterializedWorkspace> {
    let isolated_count = resolved
        .mounts
        .iter()
        .filter(|m| !m.isolation.is_shared())
        .count();
    debug_log!(
        "isolation",
        "materialize_workspace: workspace={workspace_name} container={container_name} selector={selector_key} mounts={total} isolated={isolated_count} state_dir={state_dir} force={force} interactive={interactive}",
        total = resolved.mounts.len(),
        state_dir = container_state_dir.display(),
        force = ctx.force,
        interactive = ctx.interactive,
    );

    // Sort by dst length ascending so parents materialize before children
    // (depth ordering for the bind-mount stack).
    let mut indexed: Vec<(usize, &MountConfig)> = resolved.mounts.iter().enumerate().collect();
    indexed.sort_by_key(|(_, m)| m.dst.trim_end_matches('/').len());

    let mut materialized: Vec<Option<MaterializedMount>> =
        (0..resolved.mounts.len()).map(|_| None).collect();

    for (idx, mount) in indexed {
        let m = match mount.isolation {
            MountIsolation::Shared => {
                debug_log!(
                    "isolation",
                    "mount {dst}: shared (passthrough bind from {src})",
                    dst = mount.dst,
                    src = mount.src,
                );
                MaterializedMount {
                    bind_src: mount.src.clone(),
                    dst: mount.dst.clone(),
                    readonly: mount.readonly,
                    isolation: MountIsolation::Shared,
                    worktree_aux: None,
                }
            }
            MountIsolation::Worktree => {
                materialize_one(
                    mount,
                    container_state_dir,
                    selector_key,
                    container_name,
                    workspace_name,
                    ctx,
                    runner,
                )
                .await?
            }
            MountIsolation::Clone => {
                materialize_clone(
                    mount,
                    container_state_dir,
                    selector_key,
                    container_name,
                    workspace_name,
                    ctx,
                    runner,
                )
                .await?
            }
        };
        materialized[idx] = Some(m);
    }

    // Re-emit in original order — Docker mount-flag order is settled later.
    let mounts: Vec<MaterializedMount> = materialized
        .into_iter()
        .map(|m| m.expect("every mount index populated by the loop above"))
        .collect();
    Ok(MaterializedWorkspace {
        workdir: resolved.workdir.clone(),
        mounts,
        keep_awake_enabled: resolved.keep_awake_enabled,
    })
}

#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn materialize_one(
    mount: &MountConfig,
    container_state_dir: &Path,
    selector_key: &str,
    container_name: &str,
    workspace_name: &str,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<MaterializedMount> {
    let worktree_path = worktree_path_for(container_state_dir, &mount.dst, container_name);
    debug_log!(
        "isolation",
        "mount {dst}: worktree (src={src} → worktree_path={wt})",
        dst = mount.dst,
        src = mount.src,
        wt = worktree_path.display(),
    );

    // Drift guard: if a record exists, src must match.
    if let Some(record) = read_record(container_state_dir, &mount.dst)? {
        if record.original_src != mount.src {
            debug_log!(
                "isolation",
                "mount {dst}: source drift detected (recorded={recorded} configured={configured})",
                dst = mount.dst,
                recorded = record.original_src,
                configured = mount.src,
            );
            anyhow::bail!(
                "source drift on container `{}`, mount `{}`: recorded src `{}` differs from configured src `{}`; preserved {} at `{}`. Restore the previous src, inspect the path above, or `jackin purge {}` to discard.",
                container_name,
                mount.dst,
                record.original_src,
                mount.src,
                record.isolation,
                record.worktree_path,
                container_name,
            );
        }
        if record.isolation != MountIsolation::Worktree {
            anyhow::bail!(
                "isolation mode drift on container `{}`, mount `{}`: recorded mode `{}` differs from configured mode `{}`; preserved {} at `{}`. Run `jackin purge {}` to discard the old isolated state before switching modes.",
                container_name,
                mount.dst,
                record.isolation,
                mount.isolation,
                record.isolation,
                record.worktree_path,
                container_name,
            );
        }
        // Reuse if worktree path looks alive (.git file or dir under it).
        if worktree_path.join(".git").exists() {
            debug_log!(
                "isolation",
                "mount {dst}: reusing existing worktree (record matches and .git present)",
                dst = mount.dst,
            );
            // Re-write override files on every load — idempotent and
            // cheap, ensures any topology refresh (e.g., container
            // rename hypothetically) lands without manual cleanup.
            let aux =
                write_git_overrides(container_state_dir, &mount.dst, container_name, &mount.src)?;
            debug_log!(
                "isolation",
                "mount {dst}: refreshed git overrides (host_git_target={target})",
                dst = mount.dst,
                target = aux.host_git_target,
            );
            return Ok(MaterializedMount {
                bind_src: worktree_path.to_string_lossy().into(),
                dst: mount.dst.clone(),
                readonly: mount.readonly,
                isolation: MountIsolation::Worktree,
                worktree_aux: Some(aux),
            });
        }
        debug_log!(
            "isolation",
            "mount {dst}: record present but worktree directory missing — recreating",
            dst = mount.dst,
        );
    }

    // Pre-flight, then enable worktree-config, then create the worktree.
    debug_log!(
        "isolation",
        "mount {dst}: running preflight checks on host repo {src}",
        dst = mount.dst,
        src = mount.src,
    );
    preflight_worktree(mount, ctx, runner).await?;

    let _ = ensure_worktree_config_enabled(std::path::Path::new(&mount.src), runner).await?;

    let host_head = runner
        .capture("git", &["-C", &mount.src, "rev-parse", "HEAD"], None)
        .await?
        .trim()
        .to_string();
    debug_log!(
        "isolation",
        "mount {dst}: host HEAD {commit}",
        dst = mount.dst,
        commit = host_head,
    );

    // No per-mount branch suffix in V1: workspace validation rejects
    // two isolated mounts on the same host repo (see
    // `validate_isolation_layout`), so each container has at most one
    // isolated mount per host repo and the scratch branch is uniquely
    // named by the selector alone.
    // Branch name = `jackin/scratch/<container>` (Model B). Container
    // name is the disambiguator because it's globally unique by jackin
    // construction; selector alone wouldn't disambiguate parallel
    // containers of the same role class (which would collide on the
    // shared host repo's `<host>/.git/refs/heads/` namespace).
    let scratch_branch = branch_name(container_name, None);
    debug_log!(
        "isolation",
        "mount {dst}: scratch branch {branch} (selector={selector})",
        dst = mount.dst,
        branch = scratch_branch,
        selector = selector_key,
    );

    if let Some(parent) = worktree_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("create parent dir for worktree at {}", parent.display()))?;
    }

    // `worktree add -b` rejects an existing branch; reuse it. Record
    // `base_commit = host_head`, NOT branch tip: if a prior session
    // committed work onto the scratch branch and the operator wiped
    // the state dir, branch tip != host HEAD, and finalize routes
    // through the upstream/[gone]/detached arms (fail-safe to
    // PreservedUnpushed) instead of the `tip == base_commit ⇒ Safe`
    // arm that would silently delete the work.
    let base_commit = if let Some(branch_tip) = find_local_branch_tip(&mount.src, &scratch_branch) {
        debug_log!(
            "isolation",
            "mount {dst}: adopting existing scratch branch {branch} at tip {tip} (host HEAD {host}); pruning orphan admin entries first",
            dst = mount.dst,
            branch = scratch_branch,
            tip = branch_tip,
            host = host_head,
        );
        runner
            .run(
                "git",
                &["-C", &mount.src, "worktree", "prune"],
                None,
                &crate::docker::RunOptions::default(),
            )
            .await?;
        debug_log!(
            "isolation",
            "mount {dst}: git -C {src} worktree add {wt} {branch}",
            dst = mount.dst,
            src = mount.src,
            branch = scratch_branch,
            wt = worktree_path.display(),
        );
        runner
            .run(
                "git",
                &[
                    "-C",
                    &mount.src,
                    "worktree",
                    "add",
                    &worktree_path.to_string_lossy(),
                    &scratch_branch,
                ],
                None,
                &crate::docker::RunOptions::default(),
            )
            .await
            .with_context(|| {
                format!(
                    "isolated mount `{}`: adopt of existing scratch branch `{}` failed; \
                     if the branch is checked out in another worktree, \
                     `git -C {} worktree list --porcelain` will show where",
                    mount.dst, scratch_branch, mount.src,
                )
            })?;
        host_head.clone()
    } else {
        debug_log!(
            "isolation",
            "mount {dst}: git -C {src} worktree add -b {branch} {wt} {base}",
            dst = mount.dst,
            src = mount.src,
            branch = scratch_branch,
            wt = worktree_path.display(),
            base = host_head,
        );
        runner
            .run(
                "git",
                &[
                    "-C",
                    &mount.src,
                    "worktree",
                    "add",
                    "-b",
                    &scratch_branch,
                    &worktree_path.to_string_lossy(),
                    &host_head,
                ],
                None,
                &crate::docker::RunOptions::default(),
            )
            .await?;
        host_head
    };

    upsert_record(
        container_state_dir,
        IsolationRecord {
            workspace: workspace_name.into(),
            mount_dst: mount.dst.clone(),
            original_src: mount.src.clone(),
            isolation: MountIsolation::Worktree,
            worktree_path: worktree_path.to_string_lossy().into(),
            scratch_branch,
            base_commit,
            selector_key: selector_key.into(),
            container_name: container_name.into(),
            cleanup_status: CleanupStatus::Active,
        },
    )?;

    let aux = write_git_overrides(container_state_dir, &mount.dst, container_name, &mount.src)?;
    debug_log!(
        "isolation",
        "mount {dst}: wrote git overrides (host_git_target={t}, git_file_target={gft}, gitdir_back_target={gbt})",
        dst = mount.dst,
        t = aux.host_git_target,
        gft = aux.git_file_target,
        gbt = aux.gitdir_back_target,
    );

    Ok(MaterializedMount {
        bind_src: worktree_path.to_string_lossy().into(),
        dst: mount.dst.clone(),
        readonly: mount.readonly,
        isolation: MountIsolation::Worktree,
        worktree_aux: Some(aux),
    })
}

#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn materialize_clone(
    mount: &MountConfig,
    container_state_dir: &Path,
    selector_key: &str,
    container_name: &str,
    workspace_name: &str,
    ctx: &PreflightContext,
    runner: &mut impl CommandRunner,
) -> anyhow::Result<MaterializedMount> {
    let clone_path = clone_path_for(container_state_dir, &mount.dst, container_name);
    debug_log!(
        "isolation",
        "mount {dst}: clone (src={src} → clone_path={cp})",
        dst = mount.dst,
        src = mount.src,
        cp = clone_path.display(),
    );

    if let Some(record) = read_record(container_state_dir, &mount.dst)? {
        if record.original_src != mount.src {
            anyhow::bail!(
                "source drift on container `{}`, mount `{}`: recorded src `{}` differs from configured src `{}`; preserved {} at `{}`. Restore the previous src, inspect the path above, or `jackin purge {}` to discard.",
                container_name,
                mount.dst,
                record.original_src,
                mount.src,
                record.isolation,
                record.worktree_path,
                container_name,
            );
        }
        if record.isolation != MountIsolation::Clone {
            anyhow::bail!(
                "isolation mode drift on container `{}`, mount `{}`: recorded mode `{}` differs from configured mode `{}`; preserved {} at `{}`. Run `jackin purge {}` to discard the old isolated state before switching modes.",
                container_name,
                mount.dst,
                record.isolation,
                mount.isolation,
                record.isolation,
                record.worktree_path,
                container_name,
            );
        }
        if clone_path.join(".git").exists() {
            debug_log!(
                "isolation",
                "mount {dst}: reusing existing clone (record matches and .git present)",
                dst = mount.dst,
            );
            return Ok(MaterializedMount {
                bind_src: clone_path.to_string_lossy().into(),
                dst: mount.dst.clone(),
                readonly: mount.readonly,
                isolation: MountIsolation::Clone,
                worktree_aux: None,
            });
        }
        debug_log!(
            "isolation",
            "mount {dst}: record present but clone directory missing — recreating",
            dst = mount.dst,
        );
    }

    preflight_isolated(mount, ctx, runner).await?;

    let host_head = runner
        .capture("git", &["-C", &mount.src, "rev-parse", "HEAD"], None)
        .await?
        .trim()
        .to_string();
    let scratch_branch = branch_name(container_name, None);

    if let Some(parent) = clone_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("create parent dir for clone at {}", parent.display()))?;
    }

    runner
        .run(
            "git",
            &[
                "clone",
                "--local",
                &mount.src,
                &clone_path.to_string_lossy(),
            ],
            None,
            &crate::docker::RunOptions::default(),
        )
        .await?;
    runner
        .run(
            "git",
            &[
                "-C",
                &clone_path.to_string_lossy(),
                "checkout",
                "-B",
                &scratch_branch,
                &host_head,
            ],
            None,
            &crate::docker::RunOptions::default(),
        )
        .await?;

    // `git clone --local <mount.src>` points the clone's `origin` at
    // `mount.src` — on jackin's mount layout that path is identical
    // inside and outside the container, so pushes loop back to a
    // host-local working tree instead of the operator's upstream.
    // Copy the host's own `origin` URL across (worktree mode inherits
    // it via shared `.git/config`; clone mode has to do it by hand).
    // GitHub SCP / `ssh://` forms run through `normalize_github_url`
    // so the container's `gh` credential helper can authenticate
    // without an SSH key; embedded `userinfo@` credentials are
    // stripped so a host-side PAT does not leak into the per-container
    // `.git/config`.
    let host_origin = match runner
        .capture(
            "git",
            &["-C", &mount.src, "remote", "get-url", "origin"],
            None,
        )
        .await
    {
        Ok(s) => {
            let trimmed = s.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(strip_userinfo(crate::runtime::normalize_github_url(
                    trimmed,
                )))
            }
        }
        Err(err) => {
            // `error: No such remote 'origin'` (fresh init, never
            // pushed) is a legitimate fall-through — agent has no
            // upstream anyway. Anything else (permission denied,
            // corrupt config) aborts the launch; silently landing on
            // a loopback origin would misroute the operator's pushes.
            let chain = format!("{err:#}");
            if chain.contains("No such remote") || chain.contains("no such remote") {
                debug_log!(
                    "isolation",
                    "mount {dst}: host repo has no `origin` remote; leaving clone origin at bind-mount path",
                    dst = mount.dst,
                );
                None
            } else {
                return Err(err).with_context(|| {
                    format!(
                        "isolated mount `{}`: failed to read host repo `{}` origin URL — \
                         refusing to launch with a loopback origin that would silently \
                         misroute pushes. If the host repo legitimately has no origin, \
                         add one (`git remote add origin <url>`) or switch this mount to \
                         `isolation = \"shared\"` / `\"worktree\"`",
                        mount.dst, mount.src,
                    )
                });
            }
        }
    };

    if let Some(url) = host_origin {
        debug_log!(
            "isolation",
            "mount {dst}: rewriting clone origin from {src} to {url}",
            dst = mount.dst,
            src = mount.src,
            url = url,
        );
        runner
            .run(
                "git",
                &[
                    "-C",
                    &clone_path.to_string_lossy(),
                    "remote",
                    "set-url",
                    "origin",
                    &url,
                ],
                None,
                &crate::docker::RunOptions::default(),
            )
            .await?;
    } else {
        debug_log!(
            "isolation",
            "mount {dst}: host repo has no origin remote — leaving clone origin at bind-mount path",
            dst = mount.dst,
        );
    }

    upsert_record(
        container_state_dir,
        IsolationRecord {
            workspace: workspace_name.into(),
            mount_dst: mount.dst.clone(),
            original_src: mount.src.clone(),
            isolation: MountIsolation::Clone,
            worktree_path: clone_path.to_string_lossy().into(),
            scratch_branch,
            base_commit: host_head,
            selector_key: selector_key.into(),
            container_name: container_name.into(),
            cleanup_status: CleanupStatus::Active,
        },
    )?;

    Ok(MaterializedMount {
        bind_src: clone_path.to_string_lossy().into(),
        dst: mount.dst.clone(),
        readonly: mount.readonly,
        isolation: MountIsolation::Clone,
        worktree_aux: None,
    })
}

/// Order mounts so parents appear before children. Docker overlays later
/// mounts on earlier ones, so this lets a shared cache child mount land
/// inside an isolated worktree parent.
pub fn mount_order_for_docker(mat: &MaterializedWorkspace) -> Vec<&MaterializedMount> {
    let mut ordered: Vec<&MaterializedMount> = mat.mounts.iter().collect();
    ordered.sort_by_key(|m| m.dst.trim_end_matches('/').len());
    ordered
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[tokio::test]
    async fn materialized_mount_holds_isolation() {
        let m = MaterializedMount {
            bind_src: "/tmp/a".into(),
            dst: "/workspace/a".into(),
            readonly: false,
            isolation: MountIsolation::Worktree,
            worktree_aux: None,
        };
        assert_eq!(m.isolation, MountIsolation::Worktree);
    }

    #[tokio::test]
    async fn worktree_path_uses_container_name_as_basename() {
        // Container name as the worktree's basename gives globally
        // unique admin entry names in `<host_repo>/.git/worktrees/`.
        // The `worktree/repo/` segments mark git's territory inside
        // the per-container state dir.
        let base = PathBuf::from("/data/jackin-x");
        assert_eq!(
            worktree_path_for(&base, "/workspace/jackin", "jackin-the-architect"),
            PathBuf::from("/data/jackin-x/git/worktree/repo/workspace/jackin/jackin-the-architect"),
        );
    }

    #[tokio::test]
    async fn worktree_path_strips_trailing_slash_in_dst() {
        let base = PathBuf::from("/data/jackin-x");
        assert_eq!(
            worktree_path_for(&base, "/workspace/jackin/", "jackin-x"),
            PathBuf::from("/data/jackin-x/git/worktree/repo/workspace/jackin/jackin-x"),
        );
    }

    #[tokio::test]
    async fn clone_path_uses_clone_repo_layout() {
        let base = PathBuf::from("/data/jackin-x");
        assert_eq!(
            clone_path_for(&base, "/workspace/jackin", "jackin-x"),
            PathBuf::from("/data/jackin-x/git/clone/repo/workspace/jackin/jackin-x"),
        );
    }

    #[tokio::test]
    async fn container_host_git_path_mirrors_dst_under_jackin_host() {
        assert_eq!(
            container_host_git_path("/Users/donbeave/Projects/jackin-project/jackin"),
            "/jackin/host/Users/donbeave/Projects/jackin-project/jackin/.git",
            "host .git destination mirrors host topology under /jackin/host/, ends in .git",
        );
        assert_eq!(
            container_host_git_path("/workspace/jackin/"),
            "/jackin/host/workspace/jackin/.git",
            "trailing slash on dst is stripped",
        );
    }

    #[tokio::test]
    async fn strip_userinfo_removes_pat_from_https_origin() {
        assert_eq!(
            strip_userinfo("https://x-access-token:ghp_xxx@github.com/owner/repo.git".into()),
            "https://github.com/owner/repo.git",
        );
        assert_eq!(
            strip_userinfo("https://oauth2:token@gitlab.example.com/team/repo".into()),
            "https://gitlab.example.com/team/repo",
        );
        assert_eq!(
            strip_userinfo("http://user:pass@example.com/path".into()),
            "http://example.com/path",
        );
    }

    #[tokio::test]
    async fn strip_userinfo_passes_clean_urls_through_unchanged() {
        for url in [
            "https://github.com/owner/repo",
            "https://github.com/owner/repo.git",
            "git@github.com:owner/repo.git",
            "ssh://git@github.com/owner/repo",
            "/host/local/path",
        ] {
            assert_eq!(strip_userinfo(url.into()), url);
        }
    }

    #[tokio::test]
    async fn strip_userinfo_handles_authority_only_urls() {
        // No path component after the authority — still strip userinfo.
        assert_eq!(
            strip_userinfo("https://user:pw@github.com".into()),
            "https://github.com",
        );
    }

    #[tokio::test]
    async fn host_git_paths_disambiguate_per_mount_in_one_container() {
        // Two isolated mounts on different host repos in the same
        // container must land at distinct container paths so multi-mount
        // workspaces don't collide. With the admin entry living
        // natively inside the host `.git/` mount, this single check is
        // enough — admin disambiguation is inherited from the host
        // mount disambiguation.
        assert_ne!(
            container_host_git_path("/workspace/proj-a"),
            container_host_git_path("/workspace/proj-b"),
        );
    }

    #[tokio::test]
    async fn write_git_overrides_writes_two_files_with_correct_content() {
        let cdir = tempfile::TempDir::new().unwrap();

        let aux = write_git_overrides(
            cdir.path(),
            "/workspace/jackin",
            "jackin-the-architect",
            "/host/jackin",
        )
        .unwrap();

        // Auxiliary mount metadata reflects the design doc topology:
        // /jackin/host/<dst-tree>/.git for the host repo's .git/ mount,
        // with both override files layered on top of that mount.
        assert_eq!(aux.host_git_dir, "/host/jackin/.git");
        assert_eq!(aux.host_git_target, "/jackin/host/workspace/jackin/.git");
        assert_eq!(aux.git_file_target, "/workspace/jackin/.git");
        // gitdir back-pointer override target lives natively at
        // worktrees/<container>/gitdir inside the host .git/ mount.
        assert_eq!(
            aux.gitdir_back_target,
            "/jackin/host/workspace/jackin/.git/worktrees/jackin-the-architect/gitdir",
        );

        // Override file contents.
        let git_file = std::fs::read_to_string(&aux.git_file_override).unwrap();
        assert_eq!(
            git_file, "gitdir: /jackin/host/workspace/jackin/.git/worktrees/jackin-the-architect\n",
            "git-file redirects gitdir to the admin entry inside the host .git/ mount",
        );
        let gitdir_back = std::fs::read_to_string(&aux.gitdir_back_override).unwrap();
        assert_eq!(gitdir_back, "/workspace/jackin/.git\n");

        // Override files live under git/overrides/<dst-tree>/ on host,
        // with filenames matching their docker mount destinations
        // (.git, gitdir). No commondir override — git's on-disk default
        // (`commondir = ../..`) resolves correctly because the admin
        // entry is in-place inside the host .git/ mount.
        assert!(
            aux.git_file_override
                .ends_with("/git/overrides/workspace/jackin/.git"),
            "got {}",
            aux.git_file_override
        );
        assert!(
            aux.gitdir_back_override
                .ends_with("/git/overrides/workspace/jackin/gitdir"),
            "got {}",
            aux.gitdir_back_override
        );
    }

    #[tokio::test]
    async fn write_git_overrides_is_idempotent() {
        let cdir = tempfile::TempDir::new().unwrap();

        let first = write_git_overrides(
            cdir.path(),
            "/workspace/jackin",
            "jackin-the-architect",
            "/host/jackin",
        )
        .unwrap();
        let second = write_git_overrides(
            cdir.path(),
            "/workspace/jackin",
            "jackin-the-architect",
            "/host/jackin",
        )
        .unwrap();
        // Same paths, same content — re-running on a reused worktree is safe.
        assert_eq!(first, second);
    }

    use crate::runtime::test_support::FakeRunner;
    use std::collections::VecDeque;

    fn fake_with_outputs(outputs: &[&str]) -> FakeRunner {
        FakeRunner {
            capture_queue: VecDeque::from(
                outputs.iter().map(ToString::to_string).collect::<Vec<_>>(),
            ),
            ..Default::default()
        }
    }

    #[tokio::test]
    async fn worktree_config_skips_when_already_enabled() {
        let mut runner = fake_with_outputs(&["true\n"]);
        let newly = ensure_worktree_config_enabled(Path::new("/repo"), &mut runner)
            .await
            .unwrap();
        assert!(!newly);
        assert_eq!(runner.run_recorded.len(), 0);
    }

    #[tokio::test]
    async fn worktree_config_enables_and_bumps_format_version_from_zero() {
        let mut runner = fake_with_outputs(&["", "0"]);
        let newly = ensure_worktree_config_enabled(Path::new("/repo"), &mut runner)
            .await
            .unwrap();
        assert!(newly);
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("core.repositoryformatversion 1"))
        );
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("extensions.worktreeConfig true"))
        );
    }

    #[tokio::test]
    async fn worktree_config_skips_format_bump_when_already_one() {
        let mut runner = fake_with_outputs(&["", "1"]);
        ensure_worktree_config_enabled(Path::new("/repo"), &mut runner)
            .await
            .unwrap();
        assert!(
            !runner
                .run_recorded
                .iter()
                .any(|c| c.contains("core.repositoryformatversion"))
        );
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("extensions.worktreeConfig true"))
        );
    }

    use crate::workspace::MountConfig;

    fn ctx() -> PreflightContext {
        PreflightContext {
            workspace_name: "jackin".into(),
            force: false,
            interactive: false,
        }
    }

    fn worktree_mount(dst: &str, src: &str) -> MountConfig {
        MountConfig {
            src: src.into(),
            dst: dst.into(),
            readonly: false,
            isolation: MountIsolation::Worktree,
        }
    }

    #[tokio::test]
    async fn preflight_rejects_readonly() {
        let mut m = worktree_mount("/workspace/x", "/tmp/x");
        m.readonly = true;
        let mut runner = FakeRunner::default();
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("cannot be readonly"));
    }

    #[tokio::test]
    async fn preflight_rejects_sensitive_mount() {
        let home = directories::BaseDirs::new()
            .unwrap()
            .home_dir()
            .to_path_buf();
        let m = worktree_mount("/workspace/ssh", &home.join(".ssh").to_string_lossy());
        let mut runner = FakeRunner::default();
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("sensitive"));
    }

    #[tokio::test]
    async fn preflight_rejects_mid_rebase() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".git/rebase-merge")).unwrap();
        let m = worktree_mount("/workspace/x", &dir.path().to_string_lossy());
        let mut runner = FakeRunner::default();
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("mid-rebase-merge"));
    }

    #[tokio::test]
    async fn preflight_rejects_mid_merge() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".git")).unwrap();
        std::fs::write(dir.path().join(".git/MERGE_HEAD"), "x").unwrap();
        let m = worktree_mount("/workspace/x", &dir.path().to_string_lossy());
        let mut runner = FakeRunner::default();
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("mid-MERGE_HEAD"));
    }

    #[tokio::test]
    async fn preflight_rejects_mid_cherry_pick() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".git")).unwrap();
        std::fs::write(dir.path().join(".git/CHERRY_PICK_HEAD"), "x").unwrap();
        let m = worktree_mount("/workspace/x", &dir.path().to_string_lossy());
        let mut runner = FakeRunner::default();
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("mid-CHERRY_PICK_HEAD"));
    }

    #[tokio::test]
    async fn preflight_rejects_subdir_of_repo() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".git")).unwrap();
        let sub = dir.path().join("sub");
        std::fs::create_dir_all(&sub).unwrap();
        let m = worktree_mount("/workspace/x", &sub.to_string_lossy());
        let mut runner = fake_with_outputs(&[&dir.path().to_string_lossy()]);
        let err = preflight_worktree(&m, &ctx(), &mut runner)
            .await
            .unwrap_err();
        assert!(err.to_string().contains("not its root"));
    }

    fn dirty_porcelain() -> &'static str {
        " M src/foo.rs\n?? new.rs\n"
    }

    fn ignored_only_porcelain() -> &'static str {
        ""
    }

    fn make_repo_root() -> tempfile::TempDir {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".git")).unwrap();
        dir
    }

    fn fake_with_repo_and_status(repo: &std::path::Path, status: &str) -> FakeRunner {
        // Capture queue order: rev-parse --show-toplevel, status --porcelain
        fake_with_outputs(&[&repo.to_string_lossy(), status])
    }

    #[tokio::test]
    async fn dirty_tree_rejected_non_interactive_no_force() {
        let repo = make_repo_root();
        let m = worktree_mount("/workspace/x", &repo.path().to_string_lossy());
        let mut runner = fake_with_repo_and_status(repo.path(), dirty_porcelain());
        let mut c = ctx();
        c.force = false;
        c.interactive = false;
        let err = preflight_worktree(&m, &c, &mut runner).await.unwrap_err();
        assert!(err.to_string().contains("dirty"));
        assert!(err.to_string().contains("--force"));
    }

    #[tokio::test]
    async fn dirty_tree_passes_with_force_non_interactive() {
        let repo = make_repo_root();
        let m = worktree_mount("/workspace/x", &repo.path().to_string_lossy());
        let mut runner = fake_with_repo_and_status(repo.path(), dirty_porcelain());
        let mut c = ctx();
        c.force = true;
        preflight_worktree(&m, &c, &mut runner).await.unwrap();
    }

    #[tokio::test]
    async fn clean_tree_passes() {
        let repo = make_repo_root();
        let m = worktree_mount("/workspace/x", &repo.path().to_string_lossy());
        let mut runner = fake_with_repo_and_status(repo.path(), ignored_only_porcelain());
        preflight_worktree(&m, &ctx(), &mut runner).await.unwrap();
    }

    use crate::isolation::state::{CleanupStatus, read_records};
    use crate::workspace::ResolvedWorkspace;

    fn resolved_with_one_isolated(repo: &std::path::Path, dst: &str) -> ResolvedWorkspace {
        ResolvedWorkspace {
            label: "jackin".into(),
            workdir: dst.into(),
            mounts: vec![MountConfig {
                src: repo.to_string_lossy().into(),
                dst: dst.into(),
                readonly: false,
                isolation: MountIsolation::Worktree,
            }],
            default_agent: None,
            keep_awake_enabled: false,
            git_pull_on_entry: false,
        }
    }

    fn resolved_with_one_clone(repo: &std::path::Path, dst: &str) -> ResolvedWorkspace {
        ResolvedWorkspace {
            label: "jackin".into(),
            workdir: dst.into(),
            mounts: vec![MountConfig {
                src: repo.to_string_lossy().into(),
                dst: dst.into(),
                readonly: false,
                isolation: MountIsolation::Clone,
            }],
            default_agent: None,
            keep_awake_enabled: false,
            git_pull_on_entry: false,
        }
    }

    #[tokio::test]
    async fn first_materialization_runs_worktree_add_and_writes_record() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-the-architect");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_isolated(repo.path(), "/workspace/jackin");
        // capture queue (in order materialize_workspace will request):
        //   preflight: rev-parse --show-toplevel
        //   preflight: status --porcelain
        //   ensure_worktree_config: extensions.worktreeConfig --get
        //   ensure_worktree_config: core.repositoryformatversion --get
        //   rev-parse HEAD
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel (preflight)
            "",                             // status --porcelain (clean)
            "",                             // extensions.worktreeConfig --get (not enabled)
            "0",                            // core.repositoryformatversion --get
            "deadbeef\n",                   // rev-parse HEAD
        ]);

        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "the-architect",
            "jackin-the-architect",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        assert_eq!(mat.mounts.len(), 1);
        let m = &mat.mounts[0];
        assert!(
            m.bind_src
                .contains("/git/worktree/repo/workspace/jackin/jackin-the-architect"),
            "worktree subdir basename = container name, under git/worktree/repo/<dst-tree>/; got {}",
            m.bind_src
        );
        assert_eq!(m.dst, "/workspace/jackin");
        assert_eq!(m.isolation, MountIsolation::Worktree);

        // git worktree add should have been invoked.
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("worktree add"))
        );

        // record persisted; branch follows Model B (container name verbatim).
        let recs = read_records(&container_dir).unwrap();
        assert_eq!(recs.len(), 1);
        assert_eq!(recs[0].cleanup_status, CleanupStatus::Active);
        assert_eq!(recs[0].base_commit, "deadbeef");
        assert_eq!(
            recs[0].scratch_branch,
            "jackin/scratch/jackin-the-architect"
        );
    }

    #[tokio::test]
    async fn shared_mounts_pass_through_unchanged() {
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();
        let resolved = ResolvedWorkspace {
            label: "jackin".into(),
            workdir: "/workspace/x".into(),
            mounts: vec![MountConfig {
                src: "/tmp/cache".into(),
                dst: "/workspace/cache".into(),
                readonly: false,
                isolation: MountIsolation::Shared,
            }],
            default_agent: None,
            keep_awake_enabled: false,
            git_pull_on_entry: false,
        };
        let mut runner = FakeRunner::default();
        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();
        assert_eq!(mat.mounts[0].bind_src, "/tmp/cache");
        assert_eq!(mat.mounts[0].isolation, MountIsolation::Shared);
        assert!(
            runner.run_recorded.is_empty(),
            "no git ops for shared mounts"
        );
    }

    #[tokio::test]
    async fn clone_materialization_runs_local_shared_clone_and_writes_record() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
            "https://github.com/jackin-project/jackin\n", // remote get-url origin
        ]);

        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        assert_eq!(mat.mounts[0].isolation, MountIsolation::Clone);
        assert!(mat.mounts[0].worktree_aux.is_none());
        assert!(
            mat.mounts[0]
                .bind_src
                .contains("/git/clone/repo/workspace/jackin/jackin-x"),
            "got {}",
            mat.mounts[0].bind_src,
        );
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("git clone --local"))
        );
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c.contains("checkout -B jackin/scratch/jackin-x deadbeef"))
        );
        // Origin rewritten from bind-mount loopback to host's upstream.
        assert!(
            runner
                .run_recorded
                .iter()
                .any(|c| c
                    .contains("remote set-url origin https://github.com/jackin-project/jackin")),
            "expected `git remote set-url origin <upstream>` in: {:?}",
            runner.run_recorded
        );
        let recs = read_records(&container_dir).unwrap();
        assert_eq!(recs.len(), 1);
        assert_eq!(recs[0].isolation, MountIsolation::Clone);
        assert_eq!(recs[0].base_commit, "deadbeef");
    }

    #[tokio::test]
    async fn clone_materialization_normalizes_ssh_origin_to_https() {
        // SCP-form host origin would point the clone at an SSH endpoint
        // the container has no key for; rewrite must land on HTTPS so
        // `gh auth git-credential` can authenticate the push.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
            "git@github.com:jackin-project/jackin.git\n", // remote get-url origin (SCP form)
        ]);

        materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        assert!(
            runner.run_recorded.iter().any(|c| c
                .contains("remote set-url origin https://github.com/jackin-project/jackin.git")),
            "expected SCP-form origin to be normalized to HTTPS in: {:?}",
            runner.run_recorded
        );
    }

    #[tokio::test]
    async fn clone_materialization_skips_origin_rewrite_when_host_origin_is_empty() {
        // Ok-arm with whitespace-only output: `trimmed.is_empty()`
        // collapses to `None`, no rewrite emitted.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
            "   \r\n\t  ",                  // remote get-url origin (whitespace-only)
        ]);

        materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        assert!(
            !runner
                .run_recorded
                .iter()
                .any(|c| c.contains("remote set-url")),
            "expected no `git remote set-url` when host origin is empty; got: {:?}",
            runner.run_recorded
        );
    }

    #[tokio::test]
    async fn clone_materialization_falls_through_when_host_has_no_origin_remote() {
        // Err with `No such remote 'origin'` (fresh init, never pushed)
        // — fall through to loopback, do not abort.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
        ]);
        runner.fail_with.push((
            "remote get-url origin".into(),
            "fatal: No such remote 'origin'".into(),
        ));

        materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .expect("legitimate `No such remote` should fall through, not abort");

        assert!(
            !runner
                .run_recorded
                .iter()
                .any(|c| c.contains("remote set-url")),
            "expected no `git remote set-url` when host has no origin; got: {:?}",
            runner.run_recorded
        );
    }

    #[tokio::test]
    async fn clone_materialization_aborts_when_get_url_fails_unexpectedly() {
        // Permission denied / corrupt config — anything that isn't a
        // `No such remote` signal — must abort the launch rather than
        // silently fall through to a loopback origin and misroute the
        // operator's pushes.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
        ]);
        runner.fail_with.push((
            "remote get-url origin".into(),
            "fatal: unable to read config: Permission denied".into(),
        ));

        let err = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .expect_err("unexpected get-url failure should abort the launch");

        let chain = format!("{err:#}");
        assert!(
            chain.contains("failed to read host repo")
                && chain.contains("loopback origin")
                && chain.contains("misroute pushes"),
            "abort message should explain the failure mode and remediation; got: {chain}",
        );
    }

    #[tokio::test]
    async fn clone_materialization_strips_embedded_credentials_from_host_origin() {
        // Credentialed host origin (PAT baked into `.git/config`) must
        // not land verbatim in the per-container clone.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let resolved = resolved_with_one_clone(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(), // rev-parse --show-toplevel
            "",                             // status --porcelain
            "deadbeef\n",                   // rev-parse HEAD
            "https://x-access-token:ghp_secretsecretsecret@github.com/jackin-project/jackin.git\n",
        ]);

        materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        let setting = runner
            .run_recorded
            .iter()
            .find(|c| c.contains("remote set-url"))
            .expect("set-url should still run with a credential-stripped URL");
        assert!(
            setting.contains("https://github.com/jackin-project/jackin.git"),
            "set-url should target the credential-stripped URL; got: {setting}",
        );
        assert!(
            !setting.contains("ghp_secretsecretsecret") && !setting.contains("x-access-token"),
            "credentials must not appear in the set-url command; got: {setting}",
        );
    }

    #[tokio::test]
    async fn clone_reuse_skips_git_ops_when_git_dir_exists() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let dst = "/workspace/jackin";
        let cp = clone_path_for(&container_dir, dst, "jackin-x");
        std::fs::create_dir_all(cp.join(".git")).unwrap();
        crate::isolation::state::write_records(
            &container_dir,
            std::slice::from_ref(&crate::isolation::state::IsolationRecord {
                workspace: "jackin".into(),
                mount_dst: dst.into(),
                original_src: repo.path().to_string_lossy().into(),
                isolation: MountIsolation::Clone,
                worktree_path: cp.to_string_lossy().into(),
                scratch_branch: "jackin/scratch/jackin-x".into(),
                base_commit: "abc".into(),
                selector_key: "x".into(),
                container_name: "jackin-x".into(),
                cleanup_status: CleanupStatus::Active,
            }),
        )
        .unwrap();

        let resolved = resolved_with_one_clone(repo.path(), dst);
        let mut runner = FakeRunner::default();
        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();
        assert_eq!(mat.mounts[0].bind_src, cp.to_string_lossy());
        assert!(runner.run_recorded.is_empty(), "no git ops on clone reuse");
    }

    #[tokio::test]
    async fn second_materialization_with_existing_record_skips_git_ops() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let dst = "/workspace/jackin";
        let wt_path = worktree_path_for(&container_dir, dst, "jackin-x");
        std::fs::create_dir_all(&wt_path).unwrap();
        std::fs::write(wt_path.join(".git"), "gitdir: /elsewhere").unwrap();
        crate::isolation::state::write_records(
            &container_dir,
            std::slice::from_ref(&crate::isolation::state::IsolationRecord {
                workspace: "jackin".into(),
                mount_dst: dst.into(),
                original_src: repo.path().to_string_lossy().into(),
                isolation: MountIsolation::Worktree,
                worktree_path: wt_path.to_string_lossy().into(),
                scratch_branch: "jackin/scratch/x".into(),
                base_commit: "abc".into(),
                selector_key: "x".into(),
                container_name: "jackin-x".into(),
                cleanup_status: CleanupStatus::Active,
            }),
        )
        .unwrap();

        let resolved = resolved_with_one_isolated(repo.path(), dst);
        let mut runner = FakeRunner::default();
        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();
        assert_eq!(mat.mounts[0].bind_src, wt_path.to_string_lossy());
        assert!(runner.run_recorded.is_empty(), "no git ops on reuse");
    }

    #[tokio::test]
    async fn drift_when_recorded_src_differs_errors_before_git_ops() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();

        let dst = "/workspace/jackin";
        let wt_path = worktree_path_for(&container_dir, dst, "jackin-x");
        std::fs::create_dir_all(&wt_path).unwrap();
        crate::isolation::state::write_records(
            &container_dir,
            std::slice::from_ref(&crate::isolation::state::IsolationRecord {
                workspace: "jackin".into(),
                mount_dst: dst.into(),
                original_src: "/different/src".into(),
                isolation: MountIsolation::Worktree,
                worktree_path: wt_path.to_string_lossy().into(),
                scratch_branch: "jackin/scratch/x".into(),
                base_commit: "abc".into(),
                selector_key: "x".into(),
                container_name: "jackin-x".into(),
                cleanup_status: CleanupStatus::Active,
            }),
        )
        .unwrap();

        let resolved = resolved_with_one_isolated(repo.path(), dst);
        let mut runner = FakeRunner::default();
        let err = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("source drift") || msg.contains("differs"));
        assert!(msg.contains("/different/src"));
        assert!(runner.run_recorded.is_empty(), "no git ops on drift error");
    }

    // Removed test `two_isolated_mounts_same_repo_get_dst_suffixed_branches` —
    // workspace validation now rejects this case at the layout level
    // (`validate_isolation_layout` rule 2). The corresponding suffix
    // logic in materialize is gone; coverage moves to
    // `workspace::tests::isolation_layout_rejects_two_worktree_mounts_on_same_repo`.

    fn write_loose_branch(repo: &std::path::Path, branch: &str, content: &str) {
        let mut p = repo.join(".git").join("refs").join("heads");
        for seg in branch.split('/') {
            p = p.join(seg);
        }
        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
        std::fs::write(&p, content).unwrap();
    }

    fn write_packed_refs(repo: &std::path::Path, contents: &str) {
        std::fs::write(repo.join(".git").join("packed-refs"), contents).unwrap();
    }

    #[tokio::test]
    async fn find_local_branch_tip_reads_loose_ref_sha() {
        let repo = make_repo_root();
        let path = repo.path().to_string_lossy();
        assert_eq!(find_local_branch_tip(&path, "jackin/scratch/x"), None);
        write_loose_branch(repo.path(), "jackin/scratch/x", "deadbeefcafe\n");
        assert_eq!(
            find_local_branch_tip(&path, "jackin/scratch/x"),
            Some("deadbeefcafe".into()),
        );
    }

    #[tokio::test]
    async fn find_local_branch_tip_reads_packed_refs_sha() {
        let repo = make_repo_root();
        write_packed_refs(
            repo.path(),
            "# pack-refs with: peeled fully-peeled sorted\n\
             1111111111111111111111111111111111111111 refs/heads/main\n\
             2222222222222222222222222222222222222222 refs/heads/jackin/scratch/x\n\
             ^abcd1234abcd1234abcd1234abcd1234abcd1234\n",
        );
        let path = repo.path().to_string_lossy();
        assert_eq!(
            find_local_branch_tip(&path, "jackin/scratch/x"),
            Some("2222222222222222222222222222222222222222".into()),
        );
        assert_eq!(find_local_branch_tip(&path, "jackin/scratch/missing"), None);
    }

    #[tokio::test]
    async fn find_local_branch_tip_loose_ref_wins_over_packed_refs() {
        // git semantics: loose refs override packed-refs entries.
        // Critical because base_commit feeds finalize's safety
        // classifier, and a wrong SHA there can authorize deletion
        // of operator work.
        let repo = make_repo_root();
        write_loose_branch(repo.path(), "jackin/scratch/x", "1010101010101010\n");
        write_packed_refs(
            repo.path(),
            "9999999999999999999999999999999999999999 refs/heads/jackin/scratch/x\n",
        );
        assert_eq!(
            find_local_branch_tip(&repo.path().to_string_lossy(), "jackin/scratch/x"),
            Some("1010101010101010".into()),
        );
    }

    #[tokio::test]
    async fn find_local_branch_tip_rejects_symref_loose_content() {
        // `git symbolic-ref refs/heads/<x> refs/heads/main` writes
        // `ref: refs/heads/main\n`. Returning that verbatim as the
        // SHA poisons IsolationRecord.base_commit.
        let repo = make_repo_root();
        write_loose_branch(repo.path(), "jackin/scratch/x", "ref: refs/heads/main\n");
        assert_eq!(
            find_local_branch_tip(&repo.path().to_string_lossy(), "jackin/scratch/x"),
            None,
        );
    }

    #[tokio::test]
    async fn find_local_branch_tip_empty_loose_falls_through_to_packed() {
        // A 0-byte ref file (interrupted git op, third-party
        // tooling) must not yield Some("") and must not block the
        // packed-refs lookup.
        let repo = make_repo_root();
        write_loose_branch(repo.path(), "jackin/scratch/x", "");
        write_packed_refs(
            repo.path(),
            "abcdef1234567890abcdef1234567890abcdef12 refs/heads/jackin/scratch/x\n",
        );
        assert_eq!(
            find_local_branch_tip(&repo.path().to_string_lossy(), "jackin/scratch/x"),
            Some("abcdef1234567890abcdef1234567890abcdef12".into()),
        );
    }

    #[tokio::test]
    async fn find_local_branch_tip_skips_malformed_packed_refs_lines() {
        let repo = make_repo_root();
        write_packed_refs(
            repo.path(),
            "# header only\n\
             ^abcd\n\
             noseparator\n\
             1111111111111111111111111111111111111111\trefs/heads/jackin/scratch/x\n",
        );
        // Tab-separated row also resolves (split_once now matches
        // any ASCII whitespace, defensive against non-stock writers).
        assert_eq!(
            find_local_branch_tip(&repo.path().to_string_lossy(), "jackin/scratch/x"),
            Some("1111111111111111111111111111111111111111".into()),
        );
    }

    #[tokio::test]
    async fn stale_scratch_branch_is_adopted_when_record_absent() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-the-architect");
        std::fs::create_dir_all(&container_dir).unwrap();

        write_loose_branch(
            repo.path(),
            "jackin/scratch/jackin-the-architect",
            "feedbeefcafebabefeedbeefcafebabefeedbeef\n",
        );

        let resolved = resolved_with_one_isolated(repo.path(), "/workspace/jackin");
        // fake_with_outputs is positional: order must match
        // materialize_workspace's runner.capture() sequence.
        // Adopted branch tip is read directly from the loose ref —
        // no runner.capture() entry is consumed for it.
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(),
            "",
            "true\n",
            "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n",
        ]);

        let mat = materialize_workspace(
            &resolved,
            &container_dir,
            "the-architect",
            "jackin-the-architect",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();

        assert_eq!(mat.mounts.len(), 1);

        let prune_idx = runner
            .run_recorded
            .iter()
            .position(|c| c.contains("worktree prune"))
            .expect("worktree prune should be invoked on adopt");
        let add_idx = runner
            .run_recorded
            .iter()
            .position(|c| c.contains("worktree add"))
            .expect("worktree add should be invoked");
        assert!(
            prune_idx < add_idx,
            "prune must run before add; got prune@{prune_idx} add@{add_idx}: {:?}",
            runner.run_recorded,
        );
        let add = &runner.run_recorded[add_idx];
        assert!(
            !add.split_whitespace().any(|t| t == "-b" || t == "--branch"),
            "adopt path must not pass -b/--branch; got {add}",
        );
        assert!(
            add.ends_with(" jackin/scratch/jackin-the-architect"),
            "adopt add must end with the existing branch as the last positional arg; got {add}",
        );

        let recs = read_records(&container_dir).unwrap();
        assert_eq!(recs.len(), 1);
        // base_commit is host_head, NOT branch tip — see the comment
        // above the adopt arm in materialize_one. Asserting the
        // branch-tip value here would silently re-introduce the
        // data-loss regression flagged in PR #219 review.
        assert_eq!(
            recs[0].base_commit,
            "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
        );
        assert_eq!(
            recs[0].scratch_branch,
            "jackin/scratch/jackin-the-architect",
        );
    }

    #[tokio::test]
    async fn adopt_aborts_when_worktree_prune_fails() {
        // Prune failure must short-circuit before `worktree add`,
        // otherwise the add proceeds against an inconsistent admin
        // index and risks corrupting state.
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();
        write_loose_branch(repo.path(), "jackin/scratch/jackin-x", "abc123\n");
        let resolved = resolved_with_one_isolated(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(),
            "",
            "true\n",
            "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n",
        ]);
        runner.fail_on.push("worktree prune".into());

        let err = materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap_err();
        assert!(err.to_string().contains("worktree prune"));
        assert!(
            !runner
                .run_recorded
                .iter()
                .any(|c| c.contains("worktree add")),
            "worktree add must not run when prune fails; got {:?}",
            runner.run_recorded,
        );
        assert!(
            read_records(&container_dir).unwrap().is_empty(),
            "no record on prune failure",
        );
    }

    #[tokio::test]
    async fn fresh_materialization_uses_dash_b_when_branch_absent() {
        let repo = make_repo_root();
        let data = tempfile::TempDir::new().unwrap();
        let container_dir = data.path().join("jackin-x");
        std::fs::create_dir_all(&container_dir).unwrap();
        let resolved = resolved_with_one_isolated(repo.path(), "/workspace/jackin");
        let mut runner = fake_with_outputs(&[
            &repo.path().to_string_lossy(),
            "",
            "true\n",
            "cafef00dcafef00dcafef00dcafef00dcafef00d\n",
        ]);
        materialize_workspace(
            &resolved,
            &container_dir,
            "x",
            "jackin-x",
            "jackin",
            &PreflightContext {
                workspace_name: "jackin".into(),
                force: false,
                interactive: false,
            },
            &mut runner,
        )
        .await
        .unwrap();
        let add = runner
            .run_recorded
            .iter()
            .find(|c| c.contains("worktree add"))
            .expect("worktree add should have been invoked");
        assert!(
            add.split_whitespace().any(|t| t == "-b"),
            "fresh path must use -b; got {add}",
        );
        assert!(
            !runner
                .run_recorded
                .iter()
                .any(|c| c.contains("worktree prune")),
        );
        let recs = read_records(&container_dir).unwrap();
        assert_eq!(
            recs[0].base_commit,
            "cafef00dcafef00dcafef00dcafef00dcafef00d",
        );
    }

    #[tokio::test]
    async fn docker_mount_order_is_length_ascending() {
        let mat = MaterializedWorkspace {
            workdir: "/workspace".into(),
            mounts: vec![
                MaterializedMount {
                    bind_src: "/cache".into(),
                    dst: "/workspace/proj/target".into(),
                    readonly: false,
                    isolation: MountIsolation::Shared,
                    worktree_aux: None,
                },
                MaterializedMount {
                    bind_src: "/wt".into(),
                    dst: "/workspace/proj".into(),
                    readonly: false,
                    isolation: MountIsolation::Worktree,
                    worktree_aux: None,
                },
            ],
            keep_awake_enabled: false,
        };
        let ordered = mount_order_for_docker(&mat);
        assert_eq!(ordered[0].dst, "/workspace/proj");
        assert_eq!(ordered[1].dst, "/workspace/proj/target");
    }

    #[tokio::test]
    async fn docker_mount_order_is_stable_for_same_length() {
        let mat = MaterializedWorkspace {
            workdir: "/workspace".into(),
            mounts: vec![
                MaterializedMount {
                    bind_src: "/a".into(),
                    dst: "/workspace/aa".into(),
                    readonly: false,
                    isolation: MountIsolation::Shared,
                    worktree_aux: None,
                },
                MaterializedMount {
                    bind_src: "/b".into(),
                    dst: "/workspace/bb".into(),
                    readonly: false,
                    isolation: MountIsolation::Shared,
                    worktree_aux: None,
                },
            ],
            keep_awake_enabled: false,
        };
        let ordered = mount_order_for_docker(&mat);
        assert_eq!(ordered[0].dst, "/workspace/aa");
        assert_eq!(ordered[1].dst, "/workspace/bb");
    }
}
