//! Shared auth-token path resolution and IO for car-server's
//! per-launch ephemeral handshake (Parslee-ai/car-releases#32).
//!
//! Every consumer that touches the token agrees on the same path:
//! - `car-server` writes it on startup unless `--no-auth` is set
//!   (the default flipped from opt-in to opt-out on 2026-05 after
//!   a security audit)
//! - `car-ffi-common::proxy` reads it on connect so NAPI/PyO3
//!   daemon-mode just-works
//! - The colocated UI server exposes it at `GET /auth-token` so
//!   the local HTML renderer can `session.auth` before any other
//!   RPC
//! - External WS clients can read it directly with `0600` perms
//!   guaranteeing only the launching user can do so
//!
//! There is also a sibling **host token** (`host-token`, see
//! [`host_default_path`] / [`read_host`] / [`write_host`]) granting the
//! host-management role (Parslee-ai/car#254). It is deliberately **NOT**
//! exposed over `GET /auth-token` — readable only from its `0600` file —
//! so a client that can reach loopback HTTP but cannot read the user's
//! secret files (or a different local user) cannot obtain it. Do not add a
//! `/host-token` HTTP route: that would reopen the self-elevation hole.
//!
//! ## Path
//!
//! - **macOS**: `~/Library/Application Support/ai.parslee.car/auth-token`
//! - **Linux**: `$XDG_RUNTIME_DIR/ai.parslee.car/auth-token` if set,
//!   otherwise `~/.config/ai.parslee.car/auth-token`
//! - **Windows**: `%LOCALAPPDATA%\ai.parslee.car\auth-token` (per-user
//!   profile-private — Windows ACLs handle the equivalent of `0600`
//!   for files under the user's profile)
//!
//! ## File format
//!
//! Single line, no trailing newline. The token itself is 32 random
//! bytes encoded as base64url-no-pad (`base64::URL_SAFE_NO_PAD`),
//! which is 43 ASCII chars. Distinct per launch; never persisted
//! across daemon restarts.
//!
//! ## Windows: DPAPI encryption at rest (Parslee-ai/car#295)
//!
//! On Windows the file contents are **not** the plaintext token. The
//! ACL hardening in [`write_at`] brings the DACL to `chmod 0o600` parity
//! (SYSTEM + owner only) but cannot defend against a *malicious elevated
//! administrator*: `SeTakeOwnership` / `SeBackupPrivilege` let such a
//! principal re-own or backup-read the file regardless of its ACL. To
//! defeat that adversary the token bytes are encrypted with DPAPI
//! (`CryptProtectData`) under the launching user's logon secret before
//! being written, and decrypted with `CryptUnprotectData` on read. The
//! file therefore holds DPAPI ciphertext (base64url-no-pad encoded to
//! preserve the "single ASCII line" contract); taking ownership of the
//! file yields a blob only the originating user (or SYSTEM, in a service
//! context) can decrypt. Encryption/decryption is transparent — every
//! consumer keeps calling [`read`] / [`write`] and sees the plaintext
//! token. macOS and Linux are unaffected (POSIX `chmod 0o600` already
//! blocks cross-user reads on those platforms).

use std::io;
use std::path::PathBuf;

const DIR_NAME: &str = "ai.parslee.car";
const FILE_NAME: &str = "auth-token";

/// Environment variable that lets a client override the auth-token
/// source. When set and non-empty, [`read_for_client`] returns its
/// value verbatim instead of reading the per-platform token file.
///
/// Required for cross-host deployments — e.g. an FFI client running on
/// Windows talking to a daemon on a Linux host. The client has no file
/// at the local `%LOCALAPPDATA%` path because that's the daemon's
/// concern, but the daemon's token can be transferred out-of-band
/// (ssh, secrets store, vault) and surfaced here.
///
/// Companion to `CAR_DAEMON_URL` from `car_proto::daemon`; both are
/// client-side env-var overrides with the same precedence semantics:
/// env var wins when set, fall back to the default file path
/// otherwise. Empty or whitespace-only values are treated as unset —
/// see [`read_for_client`] for the full precedence rules. See
/// Parslee-ai/car#231 §8.0.3 for the cross-host gap this closes.
pub const TOKEN_ENV_VAR: &str = "CAR_AUTH_TOKEN";

/// Resolve the auth-token path for this platform. Returns
/// `Err(io::Error)` if neither HOME nor the platform-specific
/// fallback environment variable is set — in which case auth can't
/// be turned on at all.
pub fn default_path() -> io::Result<PathBuf> {
    let dir = default_dir()?;
    Ok(dir.join(FILE_NAME))
}

/// Filename of the per-launch **host** token — a sibling of the
/// auth-token in the same per-platform directory. Distinct credential
/// granting the host-management role (Parslee-ai/car#254). Unlike the
/// auth token it is **never** exposed over `GET /auth-token`; the only
/// way to read it is this `0600` file, which a different local user
/// cannot — that's what makes host-role unforgeable across users.
const HOST_FILE_NAME: &str = "host-token";

/// Resolve the host-token path for this platform (sibling of
/// [`default_path`] in the same directory).
pub fn host_default_path() -> io::Result<PathBuf> {
    Ok(default_dir()?.join(HOST_FILE_NAME))
}

/// Read the host token from [`host_default_path`]. `Ok(None)` when the
/// file is absent (host-role unavailable — e.g. `--no-auth`).
pub fn read_host() -> io::Result<Option<String>> {
    read_at(&host_default_path()?)
}

/// Write the host token to [`host_default_path`] with the same hardened
/// `0600` / Windows-ACL guarantees as [`write`] (both go through
/// [`write_at`]).
pub fn write_host(token: &str) -> io::Result<PathBuf> {
    let path = host_default_path()?;
    write_at(&path, token)?;
    Ok(path)
}

fn default_dir() -> io::Result<PathBuf> {
    #[cfg(target_os = "macos")]
    {
        if let Some(home) = std::env::var_os("HOME") {
            return Ok(PathBuf::from(home)
                .join("Library")
                .join("Application Support")
                .join(DIR_NAME));
        }
    }
    #[cfg(target_os = "linux")]
    {
        if let Some(rt) = std::env::var_os("XDG_RUNTIME_DIR") {
            return Ok(PathBuf::from(rt).join(DIR_NAME));
        }
        if let Some(home) = std::env::var_os("HOME") {
            return Ok(PathBuf::from(home).join(".config").join(DIR_NAME));
        }
    }
    #[cfg(target_os = "windows")]
    {
        if let Some(local) = std::env::var_os("LOCALAPPDATA") {
            return Ok(PathBuf::from(local).join(DIR_NAME));
        }
    }
    // Last-resort fallback for any platform that didn't match any
    // of the above branches: `$HOME/.car/`. Less standard but always
    // produces a usable path on a typical unix-style host.
    if let Some(home) = std::env::var_os("HOME") {
        return Ok(PathBuf::from(home).join(".car"));
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        "no HOME / XDG_RUNTIME_DIR / LOCALAPPDATA — can't resolve auth-token directory",
    ))
}

/// Read the token from [`default_path`]. Returns `Ok(None)` when the
/// file doesn't exist (auth disabled on the daemon side); returns
/// the token string on success.
pub fn read() -> io::Result<Option<String>> {
    read_at(&default_path()?)
}

/// Read the token from an explicit path — used by tests; production
/// callers use [`read`].
pub fn read_at(path: &std::path::Path) -> io::Result<Option<String>> {
    match std::fs::read_to_string(path) {
        Ok(s) => {
            let line = s.trim();
            // On Windows the file holds a DPAPI-protected, base64url-no-pad
            // blob (Parslee-ai/car#295); decrypt back to the plaintext
            // token. On macOS/Linux the file is the plaintext token itself.
            #[cfg(target_os = "windows")]
            {
                Ok(Some(windows_dpapi::unprotect_from_line(line)?))
            }
            #[cfg(not(target_os = "windows"))]
            {
                Ok(Some(line.to_string()))
            }
        }
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(e),
    }
}

/// Read the token for a *client* connecting to a daemon, with
/// [`TOKEN_ENV_VAR`] taking precedence over the local file path.
///
/// Precedence (env var wins, same shape as `CAR_DAEMON_URL`):
///
/// 1. `$CAR_AUTH_TOKEN` if set and non-empty → returned verbatim
/// 2. otherwise → delegates to [`read`] (the local per-platform file)
///
/// Whitespace around the env-var value is trimmed; whitespace-only
/// values are treated as unset and fall through to the file read.
/// Empty values fall through for the same reason — an explicitly
/// empty env var is almost always a shell-quoting bug, not a
/// deliberate "no token" signal.
///
/// Server-side callers (the daemon reading its own token from disk,
/// the UI server exposing it at `GET /auth-token`) **must continue to
/// use [`read`] directly**, not this function — the daemon would not
/// want a client-side env-var override to influence what it advertises
/// as its own per-launch token.
pub fn read_for_client() -> io::Result<Option<String>> {
    if let Some(env_value) = std::env::var_os(TOKEN_ENV_VAR) {
        // var_os preserves bytes verbatim. Use `to_str()` (NOT
        // `to_string_lossy`) so a non-UTF-8 env var falls through to
        // the file read rather than substituting `U+FFFD` replacement
        // characters and shipping garbage as the auth token. CAR
        // tokens are ASCII base64url-no-pad — always valid UTF-8 — so
        // a non-UTF-8 value is by definition a corrupted env var, and
        // "treat as unset" is the safer failure mode. Cheaper too:
        // `to_str()` returns `Some(&str)` without allocating when the
        // OsString is already a valid `&str`.
        if let Some(s) = env_value.to_str() {
            let trimmed = s.trim();
            if !trimmed.is_empty() {
                return Ok(Some(trimmed.to_string()));
            }
        }
    }
    read()
}

/// Write `token` atomically to [`default_path`]. Creates the parent
/// directory if missing. Sets `0600` permissions on POSIX; on Windows
/// the token is DPAPI-encrypted at rest (Parslee-ai/car#295) and the
/// file DACL is hardened to SYSTEM + owner (see [`write_at`]).
pub fn write(token: &str) -> io::Result<PathBuf> {
    let path = default_path()?;
    write_at(&path, token)?;
    Ok(path)
}

/// Write to an explicit path — used by tests; production callers
/// use [`write`].
pub fn write_at(path: &std::path::Path, token: &str) -> io::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
        }
    }
    let tmp = path.with_extension("tmp");
    // Write under tight perms first, then rename — POSIX
    // guarantees the rename is atomic on the same fs, so concurrent
    // readers never see a partial write.
    //
    // On Windows the on-disk bytes are DPAPI ciphertext, not the raw
    // token (Parslee-ai/car#295). DPAPI binds the blob to the launching
    // user's logon secret, so a malicious elevated admin who takes
    // ownership of the file reads only an undecryptable blob. The ACL
    // hardening below is kept as defense-in-depth. On macOS/Linux the
    // plaintext token is written and POSIX `chmod 0o600` is the barrier.
    #[cfg(target_os = "windows")]
    let on_disk = windows_dpapi::protect_to_line(token)?;
    #[cfg(target_os = "windows")]
    std::fs::write(&tmp, &on_disk)?;
    #[cfg(not(target_os = "windows"))]
    std::fs::write(&tmp, token)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))?;
    }
    std::fs::rename(&tmp, path)?;
    #[cfg(target_os = "windows")]
    {
        // On Windows the file inherits the per-user-profile ACL from
        // %LOCALAPPDATA%, which (despite the "per-user" framing)
        // grants `BUILTIN\Administrators: FullControl`. On a single-
        // admin dev box that's effectively the same as 0600, but on a
        // shared / managed / enterprise host with multiple admins
        // any local admin can read the token and impersonate the
        // daemon to the WS surface — a real privesc that the Unix
        // chmod 0o600 was designed to block. Drop inheritance and
        // re-grant explicit ACEs for SYSTEM + the launching user.
        // Failures are logged and swallowed: the file was written
        // successfully; weaker ACLs are a hardening miss, not a
        // functional failure. Fixes finding 2 of Parslee-ai/car#231.
        harden_windows_acl(path);
    }
    Ok(())
}

#[cfg(target_os = "windows")]
fn harden_windows_acl(path: &std::path::Path) {
    use std::process::Command;

    let path_str = match path.to_str() {
        Some(s) => s,
        None => {
            tracing::warn!(
                ?path,
                "auth-token ACL hardening skipped: path is not valid UTF-8"
            );
            return;
        }
    };

    // Order matters: grant the required ACEs first, THEN drop
    // inheritance. The reverse order would leave a window where the
    // file has no ACEs at all and the daemon can't re-open it.

    // SYSTEM via well-known SID (locale-independent).
    let _ = run_icacls(&[path_str, "/grant:r", "*S-1-5-18:F"]);

    // Launching user — USERPROFILE/USERNAME isn't guaranteed inside
    // every service context (SYSTEM-launched daemons have neither),
    // so skip this step when missing. The SYSTEM grant above keeps
    // the daemon process able to read the file.
    if let Some(username) = std::env::var_os("USERNAME") {
        if let Some(u) = username.to_str() {
            let grant = format!("{u}:F");
            let _ = run_icacls(&[path_str, "/grant:r", &grant]);
        }
    }

    // Drop inherited ACEs. After this, only the explicit ACEs granted
    // above remain — inherited BUILTIN\Administrators access is gone.
    if let Err(e) = run_icacls(&[path_str, "/inheritance:r"]) {
        tracing::warn!(?e, ?path, "auth-token ACL hardening: /inheritance:r failed");
    }

    // `/inheritance:r` only removes *inherited* ACEs. On the #231 §3.1
    // test host an *explicit* `BUILTIN\Administrators:(F)` ACE survived
    // inheritance removal (re-verified against the shipped v0.18.0
    // binary: `AreAccessRulesProtected: True`, yet Administrators still
    // listed with FullControl). It materializes when the daemon writes
    // the file while elevated — the kernel stamps an explicit owner/
    // admin ACE that is not inherited and so is immune to
    // `/inheritance:r`. Strip it explicitly via the locale-independent
    // well-known SID (S-1-5-32-544 = BUILTIN\Administrators) so the
    // final DACL is exactly SYSTEM + owner — true parity with the Unix
    // `chmod 0o600`. Remove both the allow (`:g`) and any deny (`:d`)
    // grant. No-ops cleanly when the ACE isn't present.
    let _ = run_icacls(&[path_str, "/remove:g", "*S-1-5-32-544"]);
    let _ = run_icacls(&[path_str, "/remove:d", "*S-1-5-32-544"]);

    // Belt-and-suspenders: an elevated writer leaves the file *owned*
    // by the Administrators group, and an owner retains WRITE_DAC —
    // any local admin could re-grant themselves read and then read the
    // token. Re-home ownership to the launching user so that implicit
    // owner power tracks a single principal, not the whole admin group.
    // Best-effort: setting an owner other than self needs
    // SeRestorePrivilege, which a non-elevated user-context daemon may
    // lack; the explicit ACE removal above is the load-bearing fix, so
    // a failure here is logged-and-swallowed like the rest. `USERNAME`
    // is the bare account name (same value the grant above uses) —
    // adequate for the single-admin dev/box case this targets; it can
    // be ambiguous for domain/Microsoft accounts, but icacls resolves
    // it against the local machine and a miss just leaves ownership
    // unchanged (logged).
    //
    // Scope note: this brings the *DACL* to chmod-0600 parity (only
    // SYSTEM + owner in the access list) and removes the casual
    // any-admin read path. It is NOT a guarantee against a *malicious*
    // elevated administrator — SeTakeOwnership / SeBackupPrivilege let
    // such a principal re-own or back-up-read the file regardless of
    // ACLs. That adversary is defeated by the DPAPI encryption-at-rest
    // layer (see [`windows_dpapi`] / Parslee-ai/car#295): the file holds
    // ciphertext bound to the launching user's logon secret, so taking
    // ownership of the file yields nothing decryptable. This ACL
    // hardening is retained as defense-in-depth (it keeps even the
    // ciphertext blob unreadable to non-owners on a single-admin box).
    if let Some(username) = std::env::var_os("USERNAME") {
        if let Some(u) = username.to_str() {
            if let Err(e) = run_icacls(&[path_str, "/setowner", u]) {
                tracing::warn!(?e, ?path, "auth-token ACL hardening: /setowner failed");
            }
        }
    }

    fn run_icacls(args: &[&str]) -> io::Result<()> {
        let status = Command::new("icacls").args(args).status()?;
        if !status.success() {
            return Err(io::Error::new(
                io::ErrorKind::Other,
                format!("icacls {args:?} exited with status {status}"),
            ));
        }
        Ok(())
    }
}

/// DPAPI encryption-at-rest for the Windows auth-token file
/// (Parslee-ai/car#295).
///
/// `CryptProtectData` encrypts the token bytes under the current user's
/// logon secret (no `CRYPTPROTECT_LOCAL_MACHINE` flag → per-user, not
/// per-machine). `CryptUnprotectData` reverses it. Because the key is
/// derived from the user's credentials, a *different* principal — even a
/// local administrator who has taken ownership of the file via
/// `SeTakeOwnership` / `SeBackupPrivilege` — gets back only an
/// undecryptable blob. `CRYPTPROTECT_UI_FORBIDDEN` is set so the calls
/// never block on a UI prompt in a headless/service daemon context.
///
/// The ciphertext is base64url-no-pad encoded so the on-disk file stays
/// a single ASCII line (the documented format contract). No entropy /
/// description / prompt args are supplied — they're optional and unused.
#[cfg(target_os = "windows")]
mod windows_dpapi {
    use super::io;
    use base64::Engine as _;
    use windows::core::PCWSTR;
    use windows::Win32::Foundation::{LocalFree, HLOCAL};
    use windows::Win32::Security::Cryptography::{
        CryptProtectData, CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB,
    };

    /// Encrypt `token` with DPAPI and return the base64url-no-pad
    /// ciphertext line to write to disk.
    pub(super) fn protect_to_line(token: &str) -> io::Result<String> {
        let plaintext = token.as_bytes();
        // SAFETY: `plaintext` outlives the call; `pbData` is only read
        // (cast away const for the C ABI, never mutated). `out` is
        // zeroed and populated by the API; on success we copy its bytes
        // out and `LocalFree` the API-allocated buffer.
        let in_blob = CRYPT_INTEGER_BLOB {
            cbData: plaintext.len() as u32,
            pbData: plaintext.as_ptr() as *mut u8,
        };
        let mut out = CRYPT_INTEGER_BLOB::default();

        let cipher = unsafe {
            CryptProtectData(
                &in_blob,
                // szdatadescr is a human-readable label, not security-
                // relevant; pass a null PCWSTR. (This arg is IntoParam
                // <PCWSTR>, not Option, in windows 0.54 — unlike the
                // other optional pointer args below.)
                PCWSTR::null(),
                None,
                None,
                None,
                CRYPTPROTECT_UI_FORBIDDEN,
                &mut out,
            )
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("CryptProtectData failed: {e}"),
                )
            })?;
            let bytes = std::slice::from_raw_parts(out.pbData, out.cbData as usize).to_vec();
            let _ = LocalFree(HLOCAL(out.pbData as *mut core::ffi::c_void));
            bytes
        };

        Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cipher))
    }

    /// Decrypt a base64url-no-pad DPAPI ciphertext `line` back to the
    /// plaintext token.
    pub(super) fn unprotect_from_line(line: &str) -> io::Result<String> {
        let mut cipher = base64::engine::general_purpose::URL_SAFE_NO_PAD
            .decode(line.as_bytes())
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("auth-token is not valid base64url DPAPI ciphertext: {e}"),
                )
            })?;

        let in_blob = CRYPT_INTEGER_BLOB {
            cbData: cipher.len() as u32,
            pbData: cipher.as_mut_ptr(),
        };
        let mut out = CRYPT_INTEGER_BLOB::default();

        // SAFETY: same contract as `protect_to_line` — inputs outlive
        // the call, `out` is API-allocated and `LocalFree`d after copy.
        let plaintext = unsafe {
            CryptUnprotectData(
                &in_blob,
                None,
                None,
                None,
                None,
                CRYPTPROTECT_UI_FORBIDDEN,
                &mut out,
            )
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("CryptUnprotectData failed (token not decryptable by this user): {e}"),
                )
            })?;
            let bytes = std::slice::from_raw_parts(out.pbData, out.cbData as usize).to_vec();
            let _ = LocalFree(HLOCAL(out.pbData as *mut core::ffi::c_void));
            bytes
        };

        String::from_utf8(plaintext).map_err(|e| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("decrypted auth-token is not valid UTF-8: {e}"),
            )
        })
    }
}

/// Remove the token file. Idempotent — `Ok(())` whether or not the
/// file existed. Used by the `car-server` shutdown path so a stale
/// token doesn't outlive the daemon that minted it.
pub fn remove() -> io::Result<()> {
    let path = default_path()?;
    match std::fs::remove_file(&path) {
        Ok(()) => Ok(()),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(e),
    }
}

/// Generate a fresh 32-byte token, base64url-no-pad encoded
/// (43 chars). Uses the system CSPRNG via `getrandom` indirectly
/// through `uuid::Uuid::new_v4` which seeds from `getrandom` — we
/// concatenate two UUIDs (16 bytes each) and re-encode.
pub fn generate() -> String {
    use base64::Engine as _;
    let a = uuid::Uuid::new_v4();
    let b = uuid::Uuid::new_v4();
    let mut bytes = [0u8; 32];
    bytes[..16].copy_from_slice(a.as_bytes());
    bytes[16..].copy_from_slice(b.as_bytes());
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}

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

    #[test]
    fn generated_token_is_43_chars_base64url() {
        let t = generate();
        assert_eq!(t.len(), 43);
        for c in t.chars() {
            assert!(
                c.is_ascii_alphanumeric() || c == '-' || c == '_',
                "non-base64url char: {c:?}"
            );
        }
    }

    #[test]
    fn read_at_missing_returns_none() {
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("nonexistent.token");
        let result = read_at(&path).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn write_at_then_read_at_round_trips() {
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("ai.parslee.car").join("auth-token");
        let token = generate();
        write_at(&path, &token).unwrap();
        assert_eq!(read_at(&path).unwrap().as_deref(), Some(token.as_str()));
        assert!(path.exists());
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(&path).unwrap().permissions().mode();
            assert_eq!(mode & 0o777, 0o600, "token must be 0600");
        }
    }

    #[test]
    fn write_at_overwrites_atomically() {
        // Repeated writes must replace cleanly — the rename-into-place
        // pattern guarantees no partial-write window. Two consecutive
        // writes with different content; second wins.
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("auth-token");
        write_at(&path, "first").unwrap();
        write_at(&path, "second").unwrap();
        assert_eq!(read_at(&path).unwrap().as_deref(), Some("second"));
    }

    /// #231 §3.1 regression guard: the Windows auth-token DACL must be
    /// exactly owner + SYSTEM. An elevated writer leaves an explicit
    /// `BUILTIN\Administrators:(F)` ACE behind; `harden_windows_acl`
    /// strips it (true `chmod 0o600` parity). Without this test a
    /// refactor of the icacls sequence could silently re-open the
    /// any-admin read path and nothing would catch it — the behavior
    /// shipped untested until now.
    ///
    /// Asserts against locale-independent well-known SIDs
    /// (`S-1-5-32-544` = BUILTIN\Administrators, `S-1-5-18` = SYSTEM) so
    /// the test is stable on non-English hosts where icacls/Get-Acl
    /// print localized display names. Reads the DACL back via PowerShell
    /// `Get-Acl` + `Translate(...)` to SID. Skips (does not fail) when
    /// that read-back tool is unavailable on the runner; hard-asserts
    /// when it is present. NOTE: this body only executes on a real
    /// Windows host — `check-windows` CI runs `cargo check` (compile
    /// only), so the e2e Windows test job (see ci.yml) is what actually
    /// exercises it.
    #[cfg(target_os = "windows")]
    #[test]
    fn windows_token_dacl_excludes_administrators() {
        use std::process::Command;

        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("ai.parslee.car").join("auth-token");
        let token = generate();
        write_at(&path, &token).unwrap();
        assert!(path.exists());

        let path_str = path.to_str().expect("tempdir path is UTF-8");

        // Enumerate the DACL as locale-independent SID strings. Get-Acl
        // returns NTAccount identities; Translate() canonicalizes each
        // to its SID so the assertions don't depend on display-name
        // locale. Fall back to the raw .Value if an identity can't be
        // translated (e.g. an orphaned SID), so it still appears in the
        // list we assert against.
        let script = format!(
            "$ErrorActionPreference='Stop'; \
             (Get-Acl -LiteralPath '{path_str}').Access | ForEach-Object {{ \
               try {{ $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value }} \
               catch {{ $_.IdentityReference.Value }} }}"
        );

        let output = match Command::new("powershell")
            .args(["-NoProfile", "-NonInteractive", "-Command", &script])
            .output()
        {
            Ok(o) => o,
            Err(e) => {
                eprintln!(
                    "skipping windows_token_dacl_excludes_administrators: \
                     powershell unavailable ({e})"
                );
                return;
            }
        };
        if !output.status.success() {
            eprintln!(
                "skipping windows_token_dacl_excludes_administrators: \
                 Get-Acl read-back failed: {}",
                String::from_utf8_lossy(&output.stderr)
            );
            return;
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let sids: Vec<&str> = stdout
            .lines()
            .map(|l| l.trim())
            .filter(|l| !l.is_empty())
            .collect();
        assert!(
            !sids.is_empty(),
            "Get-Acl returned no ACEs for {path_str}; read-back produced \
             nothing to assert on (treat as a test-environment failure, \
             not a pass)"
        );

        // Load-bearing: BUILTIN\Administrators must be gone.
        assert!(
            !sids.iter().any(|s| s.eq_ignore_ascii_case("S-1-5-32-544")),
            "auth-token DACL still grants BUILTIN\\Administrators \
             (S-1-5-32-544) — harden_windows_acl regression, the \
             any-admin read path is re-opened. ACEs: {sids:?}"
        );

        // SYSTEM must remain so the daemon can re-read its own token.
        assert!(
            sids.iter().any(|s| s.eq_ignore_ascii_case("S-1-5-18")),
            "auth-token DACL is missing NT AUTHORITY\\SYSTEM (S-1-5-18); \
             the daemon could lose read access to its token. ACEs: {sids:?}"
        );
    }

    /// #295 security property: on Windows the token is DPAPI-encrypted at
    /// rest, so the *raw file bytes* must NOT contain the plaintext token.
    /// This is the load-bearing defense against a malicious elevated admin
    /// who takes ownership of the file — they get ciphertext, not the
    /// secret. The round-trip (`write_at` → `read_at`) is covered by
    /// `write_at_then_read_at_round_trips`; this test guards the at-rest
    /// confidentiality that DACLs alone could not provide. Runs only on a
    /// real Windows host (`check-windows` CI is compile-only; the e2e
    /// Windows job exercises it).
    #[cfg(target_os = "windows")]
    #[test]
    fn windows_token_is_encrypted_at_rest() {
        let tmp = tempfile::TempDir::new().unwrap();
        let path = tmp.path().join("ai.parslee.car").join("auth-token");
        let token = generate();
        write_at(&path, &token).unwrap();

        let raw = std::fs::read_to_string(&path).unwrap();
        assert!(
            !raw.contains(token.trim()),
            "auth-token file contains the plaintext token — DPAPI \
             encryption-at-rest regression (#295). Raw on-disk contents \
             must be ciphertext, not the secret."
        );
        // And the round-trip still recovers the plaintext.
        assert_eq!(read_at(&path).unwrap().as_deref(), Some(token.as_str()));
    }

    // Tests that touch `$CAR_AUTH_TOKEN` (or read an env-derived path
    // like `HOME` while another test mutates it) serialize on the
    // crate-wide [`crate::env_test_lock`] — a single process-global lock
    // shared with `memory_path::tests` / `proxy::tests` so cross-module
    // env races (e.g. this module reading `HOME` while `memory_path`
    // writes it) can't happen, and so a panic in one test doesn't poison
    // a per-module lock and cascade.

    /// Panic-safe snapshot/restore helper for a single env var.
    /// Captures the current value on construction, restores it on
    /// `Drop` — so a test panic between mutation and explicit
    /// cleanup doesn't leak the override to subsequent tests.
    struct EnvGuard {
        var: &'static str,
        prev: Option<std::ffi::OsString>,
    }
    impl EnvGuard {
        fn capture(var: &'static str) -> Self {
            Self {
                var,
                prev: std::env::var_os(var),
            }
        }
    }
    impl Drop for EnvGuard {
        fn drop(&mut self) {
            match &self.prev {
                Some(v) => unsafe { std::env::set_var(self.var, v) },
                None => unsafe { std::env::remove_var(self.var) },
            }
        }
    }

    /// F2 (#231 §8.0.3): `$CAR_AUTH_TOKEN` wins over the local file
    /// path when set and non-empty. Required for cross-host clients —
    /// the FFI process can't read the daemon's token off the local
    /// disk when the daemon is on a different machine.
    #[test]
    fn read_for_client_prefers_env_var_when_set() {
        let _lock = crate::env_test_lock();
        let _env = EnvGuard::capture(TOKEN_ENV_VAR);

        unsafe { std::env::set_var(TOKEN_ENV_VAR, "from-env") };
        assert_eq!(
            read_for_client().unwrap().as_deref(),
            Some("from-env"),
            "env var should win when set"
        );
    }

    /// Empty and whitespace-only env values must NOT satisfy auth —
    /// a shell-quoting bug shouldn't silently disable the handshake.
    /// We assert this by the property that matters: `read_for_client`
    /// with an empty/whitespace env var must return *exactly the same
    /// outcome* as `read()` would with no env var at all. This holds
    /// regardless of whether the host has a token file at the
    /// well-known path, which lets the test stay hermetic — no
    /// mutation of `HOME`, `XDG_RUNTIME_DIR`, or `LOCALAPPDATA`
    /// required. (Touching those vars would create races with
    /// `memory_path::tests` and other consumers that mutate HOME
    /// outside this lock.)
    #[test]
    fn read_for_client_treats_empty_env_as_unset() {
        let _lock = crate::env_test_lock();
        let _env = EnvGuard::capture(TOKEN_ENV_VAR);

        for empty_value in ["", "   ", "\t\n", " \r\n "] {
            unsafe { std::env::set_var(TOKEN_ENV_VAR, empty_value) };
            let via_client = read_for_client();
            // Snapshot `read()` with no env interference for comparison.
            unsafe { std::env::remove_var(TOKEN_ENV_VAR) };
            let via_direct = read();
            // Restore the empty value so the next iteration's snapshot
            // semantics hold.
            unsafe { std::env::set_var(TOKEN_ENV_VAR, empty_value) };

            match (&via_client, &via_direct) {
                (Ok(a), Ok(b)) => assert_eq!(
                    a, b,
                    "with empty env value {empty_value:?}, read_for_client \
                     must match read()'s outcome (env var should be treated \
                     as unset)"
                ),
                (Err(_), Err(_)) => {} // both fail the same way — acceptable
                (a, b) => panic!(
                    "read_for_client and read disagreed for empty env value \
                     {empty_value:?}: client={a:?} direct={b:?}"
                ),
            }
        }
    }

    /// Without `$CAR_AUTH_TOKEN`, `read_for_client` behaves exactly
    /// like `read()` — no change for same-host loopback callers.
    #[test]
    fn read_for_client_falls_back_to_read_when_unset() {
        let _lock = crate::env_test_lock();
        let _env = EnvGuard::capture(TOKEN_ENV_VAR);

        unsafe { std::env::remove_var(TOKEN_ENV_VAR) };
        let via_client = read_for_client();
        let via_direct = read();
        match (&via_client, &via_direct) {
            (Ok(a), Ok(b)) => assert_eq!(
                a, b,
                "read_for_client without env var must match read() exactly"
            ),
            (Err(_), Err(_)) => {} // both fail the same way — acceptable
            (a, b) => panic!("read_for_client and read disagreed: client={a:?} direct={b:?}"),
        }
    }

    /// Trimming: the env var value is trimmed before use. A token
    /// pasted via SSH with trailing newline (the §8.0.3 standard
    /// transfer mechanism) still works. Also covers Windows-style
    /// CRLF endings if a token file was cat'd on Windows and piped
    /// over.
    #[test]
    fn read_for_client_trims_whitespace_around_token() {
        let _lock = crate::env_test_lock();
        let _env = EnvGuard::capture(TOKEN_ENV_VAR);

        for (input, expected) in [
            ("  some-token\n", "some-token"),
            ("some-token\r\n", "some-token"),
            ("\tsome-token\t", "some-token"),
        ] {
            unsafe { std::env::set_var(TOKEN_ENV_VAR, input) };
            assert_eq!(
                read_for_client().unwrap().as_deref(),
                Some(expected),
                "trim should normalize {input:?} → {expected:?}"
            );
        }
    }

    /// Non-UTF-8 env var must fall through to the file read, not
    /// substitute replacement chars and ship garbage. CAR tokens are
    /// ASCII base64url-no-pad so a non-UTF-8 value is by definition
    /// corrupt. Unix-only because `set_var` on Windows already
    /// requires UTF-16 — non-UTF-8 isn't constructible there.
    #[cfg(unix)]
    #[test]
    fn read_for_client_treats_non_utf8_env_as_unset() {
        use std::os::unix::ffi::OsStrExt;
        let _lock = crate::env_test_lock();
        let _env = EnvGuard::capture(TOKEN_ENV_VAR);

        // 0xFF is not a valid UTF-8 start byte; this OsStr cannot be
        // converted via `to_str()` and must fall through.
        let bad = std::ffi::OsStr::from_bytes(&[b'a', 0xFF, b'b']);
        unsafe { std::env::set_var(TOKEN_ENV_VAR, bad) };

        let via_client = read_for_client();
        // Snapshot read() with no env for comparison.
        unsafe { std::env::remove_var(TOKEN_ENV_VAR) };
        let via_direct = read();
        unsafe { std::env::set_var(TOKEN_ENV_VAR, bad) };

        match (&via_client, &via_direct) {
            (Ok(a), Ok(b)) => assert_eq!(
                a, b,
                "non-UTF-8 env value must be treated as unset and \
                 fall through to read()"
            ),
            (Err(_), Err(_)) => {}
            (a, b) => panic!(
                "read_for_client and read disagreed for non-UTF-8: \
                 client={a:?} direct={b:?}"
            ),
        }
    }
}
