//! Locate the runtime assets that supermachine needs to start a
//! microVM: the Linux kernel image, the in-VM init shim source +
//! prebuilt binary, and the HVF entitlements plist.
//!
//! The CLI subcommands `supermachine bundle`, `supermachine
//! codesign`, `supermachine assets-path`, and `supermachine
//! entitlements-path` use these helpers. Embedders shipping a
//! `.app` bundle should call [`AssetPaths::from_app_bundle`] with
//! the `Contents/Resources/` dir of their bundle.
//!
//! ## Discovery order
//!
//! 1. `$SUPERMACHINE_ASSETS_DIR` if set (explicit override).
//! 2. The directory containing the running binary's executable (so
//!    a `.app/Contents/MacOS/your-app` finds assets in
//!    `Contents/Resources/`).
//! 3. `$HOME/.local/share/supermachine/` (release-tarball install).
//! 4. The dev-tree path `crates/supermachine/{kernel,oci,...}`
//!    relative to the binary's ancestors (cargo workspace dev).
//! 5. **Bundled fallback** — extract the kernel + init shim that
//!    rode inside this binary (via the `supermachine-kernel`
//!    crate's `KERNEL_BYTES` / `INIT_OCI_BYTES` constants) into
//!    `$XDG_DATA_HOME/supermachine/v{VERSION}/` and return that.
//!    First call writes the files; subsequent calls reuse them.
//!    This is what makes `cargo install supermachine` work with
//!    zero manual setup — the bytes are always available, even on
//!    a host that's never seen a supermachine release tarball or
//!    dev tree.
//!
//! The first match wins. Each helper returns `Option<PathBuf>` so
//! callers can compose their own fallback strategy.

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;

/// Bundled HVF entitlements plist content. Embedded at compile
/// time so the CLI's `codesign` subcommand and `cargo-supermachine`
/// plugin work without an on-disk plist file. Customers signing
/// their own .app for distribution should write this to a temp
/// file and pass it to `codesign --entitlements`.
pub const ENTITLEMENTS_PLIST: &str = include_str!("../entitlements.plist");

/// Where supermachine looks for runtime assets, in priority order.
#[derive(Debug, Clone)]
pub struct AssetPaths {
    /// Path to the Linux kernel image. Required to start a VM.
    pub kernel: Option<PathBuf>,
    /// Path to the prebuilt in-VM init binary, if any. The CLI
    /// builds this from `oci/init-oci.c` on first bake if absent;
    /// release-tarball installs ship it pre-built.
    pub init_oci_bin: Option<PathBuf>,
    /// Path to the in-VM init source. Used as a fallback when the
    /// prebuilt binary isn't shipped (the CLI compiles it via zig).
    pub init_oci_src: Option<PathBuf>,
    /// Path to the prebuilt in-VM `supermachine-agent` binary
    /// (statically-linked aarch64-musl). Bake copies it into the
    /// delta layer at `/supermachine-agent`; init-oci forks and
    /// exec's it post-pivot for the in-guest exec / control RPCs.
    pub supermachine_agent: Option<PathBuf>,
    /// Path to the prebuilt `smpark.ko` kernel module
    /// (aarch64, vermagic-matched to [`kernel`]). Bake stages it
    /// into the initramfs at `/supermachine-smpark.ko`; init-oci
    /// loads it via `finit_module` BEFORE the rootfs pivot.
    /// Required for `vcpus > 1` reliability — see
    /// `docs/design/multi-vcpu-shipped-2026-05-18.md`.
    pub smpark_ko: Option<PathBuf>,
}

impl AssetPaths {
    /// Auto-discover assets from the standard search paths.
    pub fn discover() -> Self {
        // Honored: $SUPERMACHINE_ASSETS_DIR
        if let Some(dir) = std::env::var_os("SUPERMACHINE_ASSETS_DIR") {
            let dir = PathBuf::from(dir);
            return Self::from_dir(&dir);
        }

        let exe = std::env::current_exe().ok();

        // .app bundle: <prefix>/Contents/MacOS/your-app  →  <prefix>/Contents/Resources/
        if let Some(exe) = exe.as_deref() {
            if let Some(macos_dir) = exe.parent() {
                if macos_dir.file_name().and_then(|s| s.to_str()) == Some("MacOS") {
                    if let Some(contents) = macos_dir.parent() {
                        let res = contents.join("Resources");
                        let probe = Self::from_dir(&res);
                        if probe.kernel.is_some() {
                            return probe;
                        }
                    }
                }
            }
        }

        // Tarball install: <prefix>/bin/supermachine + <prefix>/share/supermachine/
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let share = ancestor.join("share/supermachine");
                if share.join("kernel").is_file() {
                    return Self::from_dir(&share);
                }
            }
        }

        // Dev tree: walk ancestors of the binary. Kernel lives in
        // the `supermachine-kernel` data crate; init shim sources
        // and prebuilt binaries live next to the main crate's `oci/`.
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let kernel_crate = ancestor.join("crates/supermachine-kernel");
                let main_crate = ancestor.join("crates/supermachine");
                let agent_crate = ancestor.join("crates/supermachine-guest-agent");
                if kernel_crate.join("kernel").is_file() {
                    return Self {
                        kernel: Some(kernel_crate.join("kernel")),
                        init_oci_bin: Some(main_crate.join("oci/init-oci")).filter(|p| p.is_file()),
                        init_oci_src: Some(main_crate.join("oci/init-oci.c"))
                            .filter(|p| p.is_file()),
                        supermachine_agent: Some(
                            agent_crate.join(
                                "target/aarch64-unknown-linux-musl/release/supermachine-agent",
                            ),
                        )
                        .filter(|p| p.is_file()),
                        smpark_ko: Some(main_crate.join("oci/supermachine-smpark.ko"))
                            .filter(|p| p.is_file()),
                    };
                }
            }
        }

        // Final fallback: extract the bundled bytes (rode inside
        // this binary via supermachine-kernel) into a per-user
        // data dir. This is the path that makes
        // `cargo install supermachine` produce a working CLI with
        // zero manual setup.
        if let Some(dir) = ensure_bundled_extracted() {
            let probe = Self::from_dir(&dir);
            if probe.kernel.is_some() {
                return probe;
            }
        }

        Self {
            kernel: None,
            init_oci_bin: None,
            init_oci_src: None,
            supermachine_agent: None,
            smpark_ko: None,
        }
    }

    /// Locate assets relative to a single directory (used by the
    /// `.app/Contents/Resources/` and tarball-install layouts).
    pub fn from_dir(dir: &Path) -> Self {
        Self {
            kernel: Some(dir.join("kernel")).filter(|p| p.is_file()),
            init_oci_bin: Some(dir.join("init-oci")).filter(|p| p.is_file()),
            init_oci_src: Some(dir.join("init-oci.c")).filter(|p| p.is_file()),
            supermachine_agent: Some(dir.join("supermachine-agent")).filter(|p| p.is_file()),
            // The bundled-extract path writes the module as
            // `supermachine-smpark.ko` (the same filename
            // `build_initramfs` looks for next to init-oci). Older
            // tarball/app layouts that pre-date 0.7.26 won't have
            // this file; `.filter(is_file)` keeps it as None and
            // multi-vCPU falls back to the GIC-only path.
            smpark_ko: Some(dir.join("supermachine-smpark.ko")).filter(|p| p.is_file()),
        }
    }

    /// Locate assets inside a macOS `.app` bundle. Pass the path to
    /// the `.app` itself (not its `Contents/Resources/`).
    pub fn from_app_bundle(app: &Path) -> Self {
        Self::from_dir(&app.join("Contents/Resources"))
    }
}

/// Extract the bundled kernel + init-oci bytes (linked into this
/// binary via the `supermachine-kernel` crate) to a per-user data
/// dir, returning the dir path on success. First invocation writes
/// the files; subsequent invocations short-circuit and return the
/// same dir.
///
/// Layout:
///
/// ```text
/// $XDG_DATA_HOME/supermachine/v{VERSION}/
///     kernel
///     init-oci
/// ```
///
/// Falls back to `$HOME/.local/share/...` when XDG isn't set, and
/// gives up (returns `None`) when neither HOME nor XDG_DATA_HOME
/// resolves — caller will then surface a "kernel not found" error
/// the same way it would on any other host without assets.
///
/// Errors writing the files (permission denied, disk full, ...)
/// are swallowed: we return `None` so the caller's error path
/// triggers cleanly. The next discover() attempt will retry.
fn ensure_bundled_extracted() -> Option<PathBuf> {
    let dir = bundled_assets_dir()?;
    let kernel_dst = dir.join("kernel");
    let agent_dst = dir.join("supermachine-agent");
    let kernel_hash_dst = dir.join("kernel.hash");
    let agent_hash_dst = dir.join("supermachine-agent.hash");

    let want_kernel = kernel_bytes_hash();
    let want_agent = supermachine_agent_bytes_hash();

    // init-oci + smpark.ko are aarch64/HVF-only guest assets and only exist in
    // `supermachine-kernel` on that target (the facade re-exports the aarch64
    // sub-crate). The x86_64/KVM backend builds the container rootfs via
    // overlayfs + switch_root in the generated agent initramfs and host-direct-
    // unparks vCPUs without a kernel module, so it neither has nor needs them.
    #[cfg(target_arch = "aarch64")]
    let init_dst = dir.join("init-oci");
    #[cfg(target_arch = "aarch64")]
    let smpark_dst = dir.join("supermachine-smpark.ko");
    #[cfg(target_arch = "aarch64")]
    let init_hash_dst = dir.join("init-oci.hash");
    #[cfg(target_arch = "aarch64")]
    let smpark_hash_dst = dir.join("supermachine-smpark.hash");
    #[cfg(target_arch = "aarch64")]
    let want_init = init_oci_bytes_hash();
    #[cfg(target_arch = "aarch64")]
    let want_smpark = smpark_ko_bytes_hash();

    // Cache hit: every bundled binary AND its sibling .hash file exist AND the
    // hash matches this process's expectation. Missing .hash (old install) or a
    // mismatching hash (bytes changed without a version bump — happens with
    // path deps in dev workflows) is treated as a miss and forces re-extract,
    // so a rebuilt kernel.xz isn't silently shadowed by a stale cached copy.
    let fully_extracted = || {
        let ok = kernel_dst.is_file()
            && agent_dst.is_file()
            && read_hash_file(&kernel_hash_dst).as_deref() == Some(want_kernel)
            && read_hash_file(&agent_hash_dst).as_deref() == Some(want_agent);
        #[cfg(target_arch = "aarch64")]
        let ok = ok
            && init_dst.is_file()
            && smpark_dst.is_file()
            && read_hash_file(&init_hash_dst).as_deref() == Some(want_init)
            && read_hash_file(&smpark_hash_dst).as_deref() == Some(want_smpark);
        ok
    };

    // Fast path (lock-free): everything already present + current.
    if fully_extracted() {
        return Some(dir);
    }

    if std::fs::create_dir_all(&dir).is_err() {
        return None;
    }

    // 0.7.62+ concurrent-extraction guard. On the FIRST run after a
    // version bump, multiple PROCESSES that all observe an empty
    // v{VERSION}/ dir at once — parallel test forks, or several pool
    // workers from one embedder — would each extract into the same dir
    // concurrently and race on the staging files: one process's rename
    // clobbers another's mid-flight, that `atomic_write` returns false,
    // `ensure_bundled_extracted` returns None, and the bake aborts with a
    // spurious "kernel not found". This flock serialises extractors per
    // version dir (the same shape as the SignLock guard in codesign.rs).
    // Best-effort: if the lock can't be taken (read-only dir, exotic FS)
    // we proceed unlocked — the per-process staging names in
    // `atomic_write` still prevent a torn file or a rename collision.
    let _extract_lock = ExtractLock::acquire(&dir);

    // Double-checked: a peer may have completed the extraction while we
    // blocked on the lock — skip redoing ~30 MiB of writes.
    if fully_extracted() {
        return Some(dir);
    }

    // Re-extract any asset whose on-disk hash doesn't match the
    // current process's bytes (or that's missing entirely). We
    // write the binary first, then the .hash file, so a crash
    // mid-extract leaves the .hash stale-or-missing and the next
    // run treats it as a miss and retries — never the other way
    // around (which could mask a torn binary as "valid").
    if !kernel_dst.is_file() || read_hash_file(&kernel_hash_dst).as_deref() != Some(want_kernel) {
        if !atomic_write(&dir, &kernel_dst, "kernel.partial", |tmp| {
            std::fs::write(tmp, supermachine_kernel::KERNEL_BYTES)
        }) {
            return None;
        }
        if !atomic_write_str(&dir, &kernel_hash_dst, "kernel.hash.partial", want_kernel) {
            return None;
        }
    }
    if !agent_dst.is_file() || read_hash_file(&agent_hash_dst).as_deref() != Some(want_agent) {
        if !atomic_write(&dir, &agent_dst, "supermachine-agent.partial", |tmp| {
            supermachine_kernel::extract_supermachine_agent_to(tmp)
        }) {
            return None;
        }
        if !atomic_write_str(
            &dir,
            &agent_hash_dst,
            "supermachine-agent.hash.partial",
            want_agent,
        ) {
            return None;
        }
    }
    // init-oci + smpark.ko: aarch64/HVF-only (see the binding gate above).
    #[cfg(target_arch = "aarch64")]
    {
        if !init_dst.is_file() || read_hash_file(&init_hash_dst).as_deref() != Some(want_init) {
            if !atomic_write(&dir, &init_dst, "init-oci.partial", |tmp| {
                supermachine_kernel::extract_init_oci_to(tmp)
            }) {
                return None;
            }
            if !atomic_write_str(&dir, &init_hash_dst, "init-oci.hash.partial", want_init) {
                return None;
            }
        }
        if !smpark_dst.is_file() || read_hash_file(&smpark_hash_dst).as_deref() != Some(want_smpark)
        {
            if !atomic_write(&dir, &smpark_dst, "supermachine-smpark.ko.partial", |tmp| {
                supermachine_kernel::extract_smpark_ko_to(tmp)
            }) {
                return None;
            }
            if !atomic_write_str(
                &dir,
                &smpark_hash_dst,
                "supermachine-smpark.hash.partial",
                want_smpark,
            ) {
                return None;
            }
        }
    }
    Some(dir)
}

/// Atomic write helper: stage via a sibling temp file then rename
/// into place. The writer closure gets the tmp path. `rename` is
/// atomic, so a concurrent reader sees either the old file or the
/// complete new one — never a half-written one.
///
/// The staging name is made unique per process+call (`{tmp_name}.
/// {pid}.{seq}`) so that concurrent extractors — parallel test forks
/// or pool workers racing on the same fresh v{VERSION}/ dir — never
/// collide on the staging path. With a shared name, whichever process
/// renamed second would find its staging file already moved and fail
/// the rename, bubbling up as a spurious None / "kernel not found".
/// Returns true on success.
fn atomic_write<F>(dir: &Path, dst: &Path, tmp_name: &str, write: F) -> bool
where
    F: FnOnce(&Path) -> std::io::Result<()>,
{
    static SEQ: AtomicU64 = AtomicU64::new(0);
    let seq = SEQ.fetch_add(1, Ordering::Relaxed);
    let tmp = dir.join(format!("{tmp_name}.{}.{seq}", std::process::id()));
    if write(&tmp).is_err() {
        let _ = std::fs::remove_file(&tmp);
        return false;
    }
    if std::fs::rename(&tmp, dst).is_err() {
        let _ = std::fs::remove_file(&tmp);
        return false;
    }
    true
}

fn atomic_write_str(dir: &Path, dst: &Path, tmp_name: &str, contents: &str) -> bool {
    atomic_write(dir, dst, tmp_name, |tmp| std::fs::write(tmp, contents))
}

/// Read a sibling `.hash` file and return its contents as a
/// trimmed string. Returns `None` if the file is missing,
/// unreadable, or longer than a sane bound — the caller treats
/// any of those as a cache miss and re-extracts. The size cap
/// keeps a corrupted-or-malicious cache dir from making us slurp
/// gigabytes.
fn read_hash_file(path: &Path) -> Option<String> {
    let bytes = std::fs::read(path).ok()?;
    if bytes.len() > 128 {
        return None;
    }
    Some(String::from_utf8(bytes).ok()?.trim().to_owned())
}

/// 12-hex-char SHA-256 prefix of `KERNEL_BYTES`. Hashed once per
/// process and cached in a `OnceLock` so multiple `discover()`
/// calls don't re-hash ~29 MiB every time.
fn kernel_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::KERNEL_BYTES))
}

// init-oci + smpark.ko hashes: aarch64/HVF-only assets (see
// `ensure_bundled_extracted`). Not present in the kernel facade on x86_64.
#[cfg(target_arch = "aarch64")]
fn init_oci_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::INIT_OCI_BYTES))
}

fn supermachine_agent_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::SUPERMACHINE_AGENT_BYTES))
}

#[cfg(target_arch = "aarch64")]
fn smpark_ko_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::SMPARK_KO_BYTES))
}

/// SHA-256 of `bytes`, hex-encoded, truncated to 12 chars
/// (48 bits). Enough to distinguish accidental rebuilds of the
/// same asset — not a security boundary; the cache dir is
/// already user-writable, so a hostile actor can swap files
/// regardless.
fn short_sha256(bytes: &[u8]) -> String {
    let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
    let mut s = String::with_capacity(12);
    for b in &digest.as_ref()[..6] {
        s.push_str(&format!("{:02x}", b));
    }
    s
}

/// Copy `supermachine-worker` from its discovered source path
/// (`@supermachine/core-darwin-arm64/supermachine-worker`,
/// `~/.cargo/bin/supermachine-worker`, …) into the per-version
/// user-data dir and return the user-data path. The library spawns
/// FROM THE COPY, and the codesign autopilot signs that copy — so
/// `node_modules` (or `~/.cargo/bin`, depending on install method)
/// is never mutated.
///
/// Why we need this: prior versions called `codesign --force` on
/// the npm-installed file directly. That:
///   1. Mutates a path the package manager considers immutable.
///   2. Triggers macOS `codesign` to write a `.cstemp` next to it.
///      If the user has chflags-locked the worker (a real
///      diagnostic scenario from the field), codesign can't
///      complete the atomic rename and the `.cstemp` is left
///      with `uchg` set — permanently breaking every future
///      `codesign --force` with "internal error in Code Signing
///      subsystem". `npm install` doesn't touch `.cstemp`, so the
///      breakage survives reinstalls.
///   3. Bumps mtime on the npm file every fresh install →
///      file-watchers (Watchman, VSCode reloaders) see churn.
///
/// Copying to a user-writable scratch dir isolates all that.
///
/// Idempotent on a content hash of the source: re-runs are O(1)
/// `stat` calls. A source change (npm upgrade, fresh
/// `cargo install`) bumps the hash → copy re-runs.
pub fn ensure_worker_in_user_dir(source: &Path) -> Result<PathBuf, String> {
    let dir = bundled_assets_dir().ok_or_else(|| {
        format!(
            "no $XDG_DATA_HOME or $HOME to extract worker into; \
             set $SUPERMACHINE_WORKER_BIN to a user-writable path \
             holding a v{} worker binary instead",
            env!("CARGO_PKG_VERSION")
        )
    })?;
    ensure_worker_in_dir(source, &dir)
}

/// Inner copy + atomic-rename + hash-sentinel logic, factored out
/// from [`ensure_worker_in_user_dir`] so unit tests can drive an
/// explicit `dir` without depending on `$XDG_DATA_HOME`/`$HOME`
/// process-globals (which would race other parallel tests).
pub(crate) fn ensure_worker_in_dir(source: &Path, dir: &Path) -> Result<PathBuf, String> {
    let dst = dir.join("supermachine-worker");
    let hash_dst = dir.join("supermachine-worker.hash");
    let source_hash = short_sha256_file(source)
        .map_err(|e| format!("hash worker source {}: {e}", source.display()))?;

    // Cache hit: the user-dir worker exists AND its sibling
    // .hash matches the source's content hash. Same invalidation
    // semantics as the kernel/init-oci/agent bundled-extract code
    // — a source change forces a re-copy.
    if dst.is_file() && read_hash_file(&hash_dst).as_deref() == Some(source_hash.as_str()) {
        return Ok(dst);
    }

    std::fs::create_dir_all(dir).map_err(|e| format!("mkdir -p {}: {e}", dir.display()))?;

    // Atomic write: copy to a sibling `.partial`, set executable,
    // rename into place. Same shape as the kernel/init-oci/agent
    // extraction below. Critical detail: we must NEVER leave the
    // dst path occupied by a partially-copied binary — codesign
    // would then sign 2 MB of garbage and the next bake would
    // crash on entry.
    // Per-CALL staging name (pid + counter) so concurrent copies never
    // collide on the rename — the same race fixed for asset extraction in
    // 0.7.62: with a shared name, whichever copier renamed second found
    // its staging file gone and Err'd, or two writers tore the file and
    // codesign signed garbage. pid alone is insufficient (threads within
    // one process share it), hence the counter — matching `atomic_write`.
    // Each copy renames its OWN temp atomically; last writer wins,
    // identical bytes. Also clear any stale shared-name partial a crashed
    // pre-fix run may have left so it can't accrete.
    static WORKER_COPY_SEQ: AtomicU64 = AtomicU64::new(0);
    let partial = dir.join(format!(
        "supermachine-worker.{}.{}.partial",
        std::process::id(),
        WORKER_COPY_SEQ.fetch_add(1, Ordering::Relaxed),
    ));
    let _ = std::fs::remove_file(&partial);
    let _ = std::fs::remove_file(dir.join("supermachine-worker.partial"));
    std::fs::copy(source, &partial)
        .map_err(|e| format!("copy {} -> {}: {e}", source.display(), partial.display()))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(&partial)
            .map_err(|e| format!("stat partial worker: {e}"))?
            .permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&partial, perms)
            .map_err(|e| format!("chmod partial worker: {e}"))?;
    }
    std::fs::rename(&partial, &dst).map_err(|e| {
        let _ = std::fs::remove_file(&partial);
        format!("rename {} -> {}: {e}", partial.display(), dst.display())
    })?;
    if !atomic_write_str(
        dir,
        &hash_dst,
        "supermachine-worker.hash.partial",
        &source_hash,
    ) {
        return Err(format!(
            "write {}: failed (disk full? perms?)",
            hash_dst.display()
        ));
    }
    Ok(dst)
}

/// Like [`short_sha256`] but for a file path; reads up to 64 MiB.
/// Worker binaries are ~2 MiB so the cap is generous.
fn short_sha256_file(path: &Path) -> Result<String, std::io::Error> {
    let bytes = std::fs::read(path)?;
    Ok(short_sha256(&bytes))
}

/// Resolve `$XDG_DATA_HOME/supermachine/v{VERSION}/`, falling back
/// to `$HOME/.local/share/supermachine/v{VERSION}/`. Versioned so
/// upgrading supermachine doesn't reuse stale assets — each crate
/// version gets its own dir.
fn bundled_assets_dir() -> Option<PathBuf> {
    let base = if let Some(d) = std::env::var_os("XDG_DATA_HOME") {
        PathBuf::from(d)
    } else if let Some(h) = std::env::var_os("HOME") {
        PathBuf::from(h).join(".local/share")
    } else {
        return None;
    };
    Some(
        base.join("supermachine")
            .join(format!("v{}", env!("CARGO_PKG_VERSION"))),
    )
}

/// Cross-process advisory lock that serialises bundled-asset extraction
/// into a single `v{VERSION}/` dir. Held via `flock(LOCK_EX)` on a
/// `.extract.lock` file inside the dir for the lifetime of the guard
/// (released on `Drop`, or automatically on process death — a killed
/// extractor never wedges the lock for its peers). Mirrors `SignLock`
/// in `codesign.rs`; see [`ensure_bundled_extracted`] for the race it
/// closes.
struct ExtractLock {
    fd: std::os::unix::io::RawFd,
}

impl ExtractLock {
    /// Acquire an exclusive lock on `<dir>/.extract.lock`, blocking until
    /// granted. Returns `None` if the lock file can't be created/opened
    /// or `flock` fails — the caller then proceeds unlocked (best-effort;
    /// the per-process staging names still prevent a torn file).
    fn acquire(dir: &Path) -> Option<Self> {
        use std::os::unix::io::IntoRawFd;
        let file = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(false) // flock lock file — content is irrelevant
            .open(dir.join(".extract.lock"))
            .ok()?;
        let fd = file.into_raw_fd();
        // Blocking exclusive flock. Advisory + per-open-file-description;
        // the kernel releases it on close() (our Drop) or process exit.
        let rc = unsafe { libc::flock(fd, libc::LOCK_EX) };
        if rc != 0 {
            unsafe {
                libc::close(fd);
            }
            return None;
        }
        Some(ExtractLock { fd })
    }
}

impl Drop for ExtractLock {
    fn drop(&mut self) {
        unsafe {
            libc::flock(self.fd, libc::LOCK_UN);
            libc::close(self.fd);
        }
    }
}

#[cfg(test)]
mod ensure_worker_in_dir_tests {
    //! 0.7.28 user-dir worker copy: atomic write, hash-based cache
    //! invalidation, idempotency. Tests use an explicit dir so they
    //! don't race on the `$XDG_DATA_HOME`/`$HOME` process-global.
    use super::{ensure_worker_in_dir, short_sha256_file};
    use std::io::Write;
    use std::os::unix::fs::PermissionsExt;
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicU64, Ordering};

    static TEST_ID: AtomicU64 = AtomicU64::new(0);

    fn unique_tmp_dir(label: &str) -> PathBuf {
        let id = TEST_ID.fetch_add(1, Ordering::Relaxed);
        let p = std::env::temp_dir().join(format!(
            "supermachine-userdir-test-{}-{label}-{id}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&p);
        p
    }

    fn make_worker(path: &std::path::Path, contents: &[u8]) {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).expect("mkdir parent");
        }
        let mut f = std::fs::File::create(path).expect("create worker source");
        f.write_all(contents).expect("write worker source");
    }

    #[test]
    fn first_call_copies_source_and_writes_hash_sentinel() {
        let src_dir = unique_tmp_dir("first-src");
        let dst_dir = unique_tmp_dir("first-dst");
        let src = src_dir.join("worker-source");
        let payload = b"#!/bin/sh\necho hi\n";
        make_worker(&src, payload);

        let dst = ensure_worker_in_dir(&src, &dst_dir).expect("first call");
        assert_eq!(dst, dst_dir.join("supermachine-worker"));
        assert!(dst.exists(), "dst worker should be present");

        // Content matches.
        assert_eq!(std::fs::read(&dst).unwrap(), payload);

        // Hash sentinel written + correct content.
        let hash_file = dst_dir.join("supermachine-worker.hash");
        assert!(hash_file.exists());
        let want = short_sha256_file(&src).unwrap();
        let got = std::fs::read_to_string(&hash_file).unwrap();
        assert_eq!(got.trim(), want);

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn second_call_on_unchanged_source_is_a_cache_hit() {
        let src_dir = unique_tmp_dir("hit-src");
        let dst_dir = unique_tmp_dir("hit-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"static contents");

        let dst1 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        // Capture the mtime of dst1.
        let mtime1 = std::fs::metadata(&dst1).unwrap().modified().unwrap();
        // Sleep enough that mtime would observably differ on a
        // re-copy (HFS+/APFS mtime resolution is 1 ns but file_time
        // resolution differs by FS — a 100 ms gap is safe).
        std::thread::sleep(std::time::Duration::from_millis(150));

        let dst2 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert_eq!(dst1, dst2);
        let mtime2 = std::fs::metadata(&dst2).unwrap().modified().unwrap();
        assert_eq!(
            mtime1, mtime2,
            "cache hit must NOT rewrite the file (mtime would change)"
        );

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn source_change_invalidates_cache_and_recopies() {
        let src_dir = unique_tmp_dir("inv-src");
        let dst_dir = unique_tmp_dir("inv-dst");
        let src = src_dir.join("worker-source");

        make_worker(&src, b"version 1 contents");
        let dst1 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let contents1 = std::fs::read(&dst1).unwrap();

        // Source content changes — emulates `npm install` of a new
        // version landing a fresh worker bin at the same path.
        make_worker(&src, b"version 2 contents - different");
        let dst2 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let contents2 = std::fs::read(&dst2).unwrap();

        assert_eq!(dst1, dst2);
        assert_ne!(
            contents1, contents2,
            "source content change must invalidate the cache and re-copy"
        );
        assert_eq!(
            contents2, b"version 2 contents - different",
            "dst must reflect new source content"
        );

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn copied_worker_is_executable() {
        // codesign needs the worker to be `0755` (or anything with
        // user-x bit) to set its signature — and HVF then needs it
        // to actually launch. Verify mode is set.
        let src_dir = unique_tmp_dir("exec-src");
        let dst_dir = unique_tmp_dir("exec-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"executable test");
        // Source might be 0644; what matters is the destination.
        let _ = std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o644));

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let mode = std::fs::metadata(&dst).unwrap().permissions().mode() & 0o777;
        assert_eq!(
            mode, 0o755,
            "dst worker must be 0755 regardless of source mode"
        );

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn missing_source_returns_error_not_panic() {
        let dst_dir = unique_tmp_dir("missing-dst");
        let result = ensure_worker_in_dir(
            std::path::Path::new("/does/not/exist/never/at/all"),
            &dst_dir,
        );
        assert!(result.is_err(), "missing source should error");
        let msg = result.err().unwrap();
        assert!(
            msg.contains("hash worker source") || msg.contains("copy "),
            "error should be actionable, got: {msg}"
        );

        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn dst_dir_created_on_demand() {
        // dst_dir doesn't exist yet — ensure_worker_in_dir should
        // mkdir -p, not fail.
        let src_dir = unique_tmp_dir("mkdir-src");
        // Note: dst_dir is a *nested* path that doesn't exist.
        let dst_dir = unique_tmp_dir("mkdir-dst").join("nested/deeper/v0.7.28");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"mkdir test");

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert!(dst.exists(), "ensure_worker_in_dir must mkdir -p dst dir");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(dst_dir.ancestors().nth(2).unwrap());
    }

    #[test]
    fn stale_partial_file_doesnt_block_copy() {
        // Simulate the failure mode: a previous run crashed
        // mid-copy, leaving `supermachine-worker.partial` behind.
        // Next call should clean it up and proceed.
        let src_dir = unique_tmp_dir("stale-src");
        let dst_dir = unique_tmp_dir("stale-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"stale-partial test");

        std::fs::create_dir_all(&dst_dir).unwrap();
        let stale_partial = dst_dir.join("supermachine-worker.partial");
        std::fs::write(&stale_partial, b"garbage from prior aborted run").unwrap();

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert_eq!(std::fs::read(&dst).unwrap(), b"stale-partial test");
        assert!(!stale_partial.exists(), "stale .partial must be cleaned up");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn concurrent_copies_into_same_dir_all_succeed() {
        // N threads copy the SAME source into the SAME dir at once
        // (parallel forks racing the first worker-copy after a version
        // bump). With the pre-fix shared `.partial`, whichever thread
        // renamed second lost the staging file and Err'd — or two writers
        // tore it and codesign would sign garbage. Per-process staging →
        // all succeed and dst is the complete, correct binary.
        let src_dir = unique_tmp_dir("conc-src");
        let dst_dir = unique_tmp_dir("conc-dst");
        let src = src_dir.join("worker-source");
        let payload = vec![0x5au8; 512 * 1024];
        make_worker(&src, &payload);

        let handles: Vec<_> = (0..12)
            .map(|_| {
                let src = src.clone();
                let dst_dir = dst_dir.clone();
                std::thread::spawn(move || {
                    ensure_worker_in_dir(&src, &dst_dir).map(|p| std::fs::read(p).unwrap())
                })
            })
            .collect();
        for h in handles {
            let bytes = h.join().expect("thread").expect("ensure_worker_in_dir");
            assert_eq!(
                bytes, payload,
                "every concurrent copy yields the complete binary"
            );
        }
        let leaked: Vec<_> = std::fs::read_dir(&dst_dir)
            .unwrap()
            .filter_map(|e| e.ok())
            .map(|e| e.file_name().to_string_lossy().into_owned())
            .filter(|n| n.contains(".partial"))
            .collect();
        assert!(leaked.is_empty(), "no staging temps leaked: {leaked:?}");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }
}

#[cfg(test)]
mod extraction_race_tests {
    //! 0.7.62 concurrent bundled-asset extraction: `atomic_write` must be
    //! safe when many extractors stage into the same fresh `v{VERSION}/`
    //! dir at once (the first-run-after-version-bump race), and the
    //! `ExtractLock` flock must acquire/release without wedging.
    use super::{atomic_write, ExtractLock};
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicU64, Ordering};

    static TEST_ID: AtomicU64 = AtomicU64::new(0);

    fn unique_tmp_dir(label: &str) -> PathBuf {
        let id = TEST_ID.fetch_add(1, Ordering::Relaxed);
        let p = std::env::temp_dir().join(format!(
            "supermachine-extract-test-{}-{label}-{id}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&p);
        std::fs::create_dir_all(&p).expect("mkdir test dir");
        p
    }

    #[test]
    fn atomic_write_is_concurrency_safe_on_shared_dst() {
        // N threads all stage the SAME dst with identical content,
        // mimicking concurrent extractors of the same asset. With the
        // pre-fix shared `.partial` staging name, whichever thread renamed
        // second would find its staging file already moved and fail —
        // bubbling up as None / "kernel not found". With per-process+call
        // staging names, every rename targets its own temp: all succeed,
        // the final file is complete + correct, and no temp leaks.
        let dir = unique_tmp_dir("atomic");
        let dst = dir.join("kernel");
        let payload = vec![0xABu8; 256 * 1024];

        let n = 16;
        let mut handles = Vec::new();
        for _ in 0..n {
            let dir = dir.clone();
            let dst = dst.clone();
            let payload = payload.clone();
            handles.push(std::thread::spawn(move || {
                atomic_write(&dir, &dst, "kernel.partial", |tmp| {
                    std::fs::write(tmp, &payload)
                })
            }));
        }
        let ok = handles
            .into_iter()
            .map(|h| h.join().expect("thread"))
            .filter(|&b| b)
            .count();

        assert_eq!(
            ok, n,
            "every concurrent atomic_write must succeed (no rename collision)"
        );
        assert_eq!(
            std::fs::read(&dst).unwrap(),
            payload,
            "dst is complete + correct, never torn"
        );
        let leaked: Vec<_> = std::fs::read_dir(&dir)
            .unwrap()
            .filter_map(|e| e.ok())
            .map(|e| e.file_name().to_string_lossy().into_owned())
            .filter(|n| n.contains(".partial"))
            .collect();
        assert!(leaked.is_empty(), "no staging temps leaked: {leaked:?}");

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn extract_lock_acquires_releases_and_reacquires() {
        // The flock is takeable on a writable dir, and dropping it
        // releases it so a later acquire succeeds — i.e. a finished (or
        // dead) extractor never wedges the lock for its peers.
        let dir = unique_tmp_dir("lock");
        {
            let lock = ExtractLock::acquire(&dir);
            assert!(lock.is_some(), "lock acquirable on a writable dir");
        } // drop → release
        let again = ExtractLock::acquire(&dir);
        assert!(
            again.is_some(),
            "lock re-acquirable after the holder dropped it"
        );
        drop(again);
        let _ = std::fs::remove_dir_all(&dir);
    }
}
