//! Run commands and assert on their behavior

#[cfg(feature = "color")]
use anstream::panic;

use crate::IntoData;

/// Process spawning for testing of non-interactive commands
#[derive(Debug)]
pub struct Command {
    cmd: std::process::Command,
    stdin: Option<crate::Data>,
    timeout: Option<std::time::Duration>,
    _stderr_to_stdout: bool,
    config: crate::Assert,
}

/// # Builder API
impl Command {
    /// Look up the path to a cargo-built binary within an integration test
    ///
    /// Cargo support:
    /// - `>1.94`: works
    /// - `>=1.91,<=1.93`: works with default `build-dir`
    /// - `<=1.92`: works
    ///
    /// # Panic
    ///
    /// Panics if no binary is found
    pub fn cargo_bin(name: &str) -> Self {
        Self::new(cargo_bin(name))
    }

    pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
        Self {
            cmd: std::process::Command::new(program),
            stdin: None,
            timeout: None,
            _stderr_to_stdout: false,
            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
        }
    }

    /// Constructs a new `Command` from a `std` `Command`.
    pub fn from_std(cmd: std::process::Command) -> Self {
        Self {
            cmd,
            stdin: None,
            timeout: None,
            _stderr_to_stdout: false,
            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
        }
    }

    /// Customize the assertion behavior
    pub fn with_assert(mut self, config: crate::Assert) -> Self {
        self.config = config;
        self
    }

    /// Adds an argument to pass to the program.
    ///
    /// Only one argument can be passed per use. So instead of:
    ///
    /// ```no_run
    /// # snapbox::cmd::Command::new("sh")
    /// .arg("-C /path/to/repo")
    /// # ;
    /// ```
    ///
    /// usage would be:
    ///
    /// ```no_run
    /// # snapbox::cmd::Command::new("sh")
    /// .arg("-C")
    /// .arg("/path/to/repo")
    /// # ;
    /// ```
    ///
    /// To pass multiple arguments see [`args`].
    ///
    /// [`args`]: Command::args()
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .arg("-l")
    ///         .arg("-a")
    ///         .assert()
    ///         .success();
    /// ```
    pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
        self.cmd.arg(arg);
        self
    }

    /// Adds multiple arguments to pass to the program.
    ///
    /// To pass a single argument see [`arg`].
    ///
    /// [`arg`]: Command::arg()
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .args(&["-l", "-a"])
    ///         .assert()
    ///         .success();
    /// ```
    pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
        self.cmd.args(args);
        self
    }

    /// Inserts or updates an environment variable mapping.
    ///
    /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
    /// and case-sensitive on all other platforms.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .env("PATH", "/bin")
    ///         .assert()
    ///         .failure();
    /// ```
    pub fn env(
        mut self,
        key: impl AsRef<std::ffi::OsStr>,
        value: impl AsRef<std::ffi::OsStr>,
    ) -> Self {
        self.cmd.env(key, value);
        self
    }

    /// Adds or updates multiple environment variable mappings.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    /// use std::process::Stdio;
    /// use std::env;
    /// use std::collections::HashMap;
    ///
    /// let filtered_env : HashMap<String, String> =
    ///     env::vars().filter(|&(ref k, _)|
    ///         k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
    ///     ).collect();
    ///
    /// Command::new("printenv")
    ///         .env_clear()
    ///         .envs(&filtered_env)
    ///         .assert()
    ///         .success();
    /// ```
    pub fn envs(
        mut self,
        vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
    ) -> Self {
        self.cmd.envs(vars);
        self
    }

    /// Removes an environment variable mapping.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .env_remove("PATH")
    ///         .assert()
    ///         .failure();
    /// ```
    pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
        self.cmd.env_remove(key);
        self
    }

    /// Clears the entire environment map for the child process.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .env_clear()
    ///         .assert()
    ///         .failure();
    /// ```
    pub fn env_clear(mut self) -> Self {
        self.cmd.env_clear();
        self
    }

    /// Sets the working directory for the child process.
    ///
    /// # Platform-specific behavior
    ///
    /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
    /// whether it should be interpreted relative to the parent's working
    /// directory or relative to `current_dir`. The behavior in this case is
    /// platform specific and unstable, and it's recommended to use
    /// [`canonicalize`] to get an absolute program path instead.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```no_run
    /// use snapbox::cmd::Command;
    ///
    /// Command::new("ls")
    ///         .current_dir("/bin")
    ///         .assert()
    ///         .success();
    /// ```
    ///
    /// [`canonicalize`]: std::fs::canonicalize()
    pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
        self.cmd.current_dir(dir);
        self
    }

    /// Write `buffer` to `stdin` when the `Command` is run.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use snapbox::cmd::Command;
    ///
    /// let mut cmd = Command::new("cat")
    ///     .arg("-et")
    ///     .stdin("42")
    ///     .assert()
    ///     .stdout_eq("42");
    /// ```
    pub fn stdin(mut self, stream: impl IntoData) -> Self {
        self.stdin = Some(stream.into_data());
        self
    }

    /// Error out if a timeout is reached
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .timeout(std::time::Duration::from_secs(1))
    ///     .env("sleep", "100")
    ///     .assert()
    ///     .failure();
    /// ```
    #[cfg(feature = "cmd")]
    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Merge `stderr` into `stdout`
    #[cfg(feature = "cmd")]
    pub fn stderr_to_stdout(mut self) -> Self {
        self._stderr_to_stdout = true;
        self
    }
}

/// # Run Command
impl Command {
    /// Run the command and assert on the results
    ///
    /// ```rust
    /// use snapbox::cmd::Command;
    ///
    /// let mut cmd = Command::new("cat")
    ///     .arg("-et")
    ///     .stdin("42")
    ///     .assert()
    ///     .stdout_eq("42");
    /// ```
    #[track_caller]
    #[must_use]
    pub fn assert(self) -> OutputAssert {
        let config = self.config.clone();
        match self.output() {
            Ok(output) => OutputAssert::new(output).with_assert(config),
            Err(err) => {
                panic!("Failed to spawn: {}", err)
            }
        }
    }

    /// Run the command and capture the `Output`
    #[cfg(feature = "cmd")]
    pub fn output(self) -> Result<std::process::Output, std::io::Error> {
        if self._stderr_to_stdout {
            self.single_output()
        } else {
            self.split_output()
        }
    }

    #[cfg(not(feature = "cmd"))]
    pub fn output(self) -> Result<std::process::Output, std::io::Error> {
        self.split_output()
    }

    #[cfg(feature = "cmd")]
    fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
        self.cmd.stdin(std::process::Stdio::piped());
        let (reader, writer) = os_pipe::pipe()?;
        let writer_clone = writer.try_clone()?;
        self.cmd.stdout(writer);
        self.cmd.stderr(writer_clone);
        let mut child = self.cmd.spawn()?;
        // Avoid a deadlock! This parent process is still holding open pipe
        // writers (inside the Command object), and we have to close those
        // before we read. Here we do this by dropping the Command object.
        drop(self.cmd);

        let stdin = self
            .stdin
            .as_ref()
            .map(|d| d.to_bytes())
            .transpose()
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
        let stdout = process_single_io(&mut child, reader, stdin)?;

        let status = wait(child, self.timeout)?;
        let stdout = stdout.join().unwrap().ok().unwrap_or_default();

        Ok(std::process::Output {
            status,
            stdout,
            stderr: Default::default(),
        })
    }

    fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
        self.cmd.stdin(std::process::Stdio::piped());
        self.cmd.stdout(std::process::Stdio::piped());
        self.cmd.stderr(std::process::Stdio::piped());
        let mut child = self.cmd.spawn()?;

        let stdin = self
            .stdin
            .as_ref()
            .map(|d| d.to_bytes())
            .transpose()
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
        let (stdout, stderr) = process_split_io(&mut child, stdin)?;

        let status = wait(child, self.timeout)?;
        let stdout = stdout
            .and_then(|t| t.join().unwrap().ok())
            .unwrap_or_default();
        let stderr = stderr
            .and_then(|t| t.join().unwrap().ok())
            .unwrap_or_default();

        Ok(std::process::Output {
            status,
            stdout,
            stderr,
        })
    }
}

fn process_split_io(
    child: &mut std::process::Child,
    input: Option<Vec<u8>>,
) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
    use std::io::Write;

    let stdin = input.and_then(|i| {
        child
            .stdin
            .take()
            .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
    });
    let stdout = child.stdout.take().map(threaded_read);
    let stderr = child.stderr.take().map(threaded_read);

    // Finish writing stdin before waiting, because waiting drops stdin.
    stdin.and_then(|t| t.join().unwrap().ok());

    Ok((stdout, stderr))
}

#[cfg(feature = "cmd")]
fn process_single_io(
    child: &mut std::process::Child,
    stdout: os_pipe::PipeReader,
    input: Option<Vec<u8>>,
) -> std::io::Result<Stream> {
    use std::io::Write;

    let stdin = input.and_then(|i| {
        child
            .stdin
            .take()
            .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
    });
    let stdout = threaded_read(stdout);
    debug_assert!(child.stdout.is_none());
    debug_assert!(child.stderr.is_none());

    // Finish writing stdin before waiting, because waiting drops stdin.
    stdin.and_then(|t| t.join().unwrap().ok());

    Ok(stdout)
}

type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;

fn threaded_read<R>(mut input: R) -> Stream
where
    R: std::io::Read + Send + 'static,
{
    std::thread::spawn(move || {
        let mut ret = Vec::new();
        input.read_to_end(&mut ret).map(|_| ret)
    })
}

impl From<std::process::Command> for Command {
    fn from(cmd: std::process::Command) -> Self {
        Self::from_std(cmd)
    }
}

/// Assert the state of a [`Command`]'s [`Output`].
///
/// Create an `OutputAssert` through the [`Command::assert`].
///
/// [`Output`]: std::process::Output
pub struct OutputAssert {
    output: std::process::Output,
    config: crate::Assert,
}

impl OutputAssert {
    /// Create an `Assert` for a given [`Output`].
    ///
    /// [`Output`]: std::process::Output
    pub fn new(output: std::process::Output) -> Self {
        Self {
            output,
            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
        }
    }

    /// Customize the assertion behavior
    pub fn with_assert(mut self, config: crate::Assert) -> Self {
        self.config = config;
        self
    }

    /// Access the contained [`Output`].
    ///
    /// [`Output`]: std::process::Output
    pub fn get_output(&self) -> &std::process::Output {
        &self.output
    }

    /// Ensure the command succeeded.
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .assert()
    ///     .success();
    /// ```
    #[track_caller]
    pub fn success(self) -> Self {
        if !self.output.status.success() {
            let desc = format!(
                "Expected {}, was {}",
                self.config.palette.info("success"),
                self.config
                    .palette
                    .error(display_exit_status(self.output.status))
            );

            use std::fmt::Write;
            let mut buf = String::new();
            writeln!(&mut buf, "{desc}").unwrap();
            self.write_stdout(&mut buf).unwrap();
            self.write_stderr(&mut buf).unwrap();
            panic!("{}", buf);
        }
        self
    }

    /// Ensure the command failed.
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("exit", "1")
    ///     .assert()
    ///     .failure();
    /// ```
    #[track_caller]
    pub fn failure(self) -> Self {
        if self.output.status.success() {
            let desc = format!(
                "Expected {}, was {}",
                self.config.palette.info("failure"),
                self.config.palette.error("success")
            );

            use std::fmt::Write;
            let mut buf = String::new();
            writeln!(&mut buf, "{desc}").unwrap();
            self.write_stdout(&mut buf).unwrap();
            self.write_stderr(&mut buf).unwrap();
            panic!("{}", buf);
        }
        self
    }

    /// Ensure the command aborted before returning a code.
    #[track_caller]
    pub fn interrupted(self) -> Self {
        if self.output.status.code().is_some() {
            let desc = format!(
                "Expected {}, was {}",
                self.config.palette.info("interrupted"),
                self.config
                    .palette
                    .error(display_exit_status(self.output.status))
            );

            use std::fmt::Write;
            let mut buf = String::new();
            writeln!(&mut buf, "{desc}").unwrap();
            self.write_stdout(&mut buf).unwrap();
            self.write_stderr(&mut buf).unwrap();
            panic!("{}", buf);
        }
        self
    }

    /// Ensure the command returned the expected code.
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("exit", "42")
    ///     .assert()
    ///     .code(42);
    /// ```
    #[track_caller]
    pub fn code(self, expected: i32) -> Self {
        if self.output.status.code() != Some(expected) {
            let desc = format!(
                "Expected {}, was {}",
                self.config.palette.info(expected),
                self.config
                    .palette
                    .error(display_exit_status(self.output.status))
            );

            use std::fmt::Write;
            let mut buf = String::new();
            writeln!(&mut buf, "{desc}").unwrap();
            self.write_stdout(&mut buf).unwrap();
            self.write_stderr(&mut buf).unwrap();
            panic!("{}", buf);
        }
        self
    }

    /// Ensure the command wrote the expected data to `stdout`.
    ///
    /// By default [`filters`][crate::filter] are applied, including:
    /// - `...` is a line-wildcard when on a line by itself
    /// - `[..]` is a character-wildcard when inside a line
    /// - `[EXE]` matches `.exe` on Windows
    /// - `"{...}"` is a JSON value wildcard
    /// - `"...": "{...}"` is a JSON key-value wildcard
    /// - `\` to `/`
    /// - Newlines
    ///
    /// To limit this to newline normalization for text, call [`Data::raw`][crate::Data::raw] on `expected`.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("stdout", "hello")
    ///     .env("stderr", "world")
    ///     .assert()
    ///     .stdout_eq("he[..]o");
    /// ```
    ///
    /// Can combine this with [`file!`][crate::file]
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    /// use snapbox::file;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("stdout", "hello")
    ///     .env("stderr", "world")
    ///     .assert()
    ///     .stdout_eq(file!["stdout.log"]);
    /// ```
    #[track_caller]
    pub fn stdout_eq(self, expected: impl IntoData) -> Self {
        let expected = expected.into_data();
        self.stdout_eq_inner(expected)
    }

    #[track_caller]
    #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stdout_eq`")]
    pub fn stdout_eq_(self, expected: impl IntoData) -> Self {
        self.stdout_eq(expected)
    }

    #[track_caller]
    fn stdout_eq_inner(self, expected: crate::Data) -> Self {
        let actual = self.output.stdout.as_slice().into_data();
        if let Err(err) = self.config.try_eq(Some(&"stdout"), actual, expected) {
            err.panic();
        }

        self
    }

    /// Ensure the command wrote the expected data to `stderr`.
    ///
    /// By default [`filters`][crate::filter] are applied, including:
    /// - `...` is a line-wildcard when on a line by itself
    /// - `[..]` is a character-wildcard when inside a line
    /// - `[EXE]` matches `.exe` on Windows
    /// - `"{...}"` is a JSON value wildcard
    /// - `"...": "{...}"` is a JSON key-value wildcard
    /// - `\` to `/`
    /// - Newlines
    ///
    /// To limit this to newline normalization for text, call [`Data::raw`][crate::Data::raw] on `expected`.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("stdout", "hello")
    ///     .env("stderr", "world")
    ///     .assert()
    ///     .stderr_eq("wo[..]d");
    /// ```
    ///
    /// Can combine this with [`file!`][crate::file]
    /// ```rust,no_run
    /// use snapbox::cmd::Command;
    /// use snapbox::cmd::cargo_bin;
    /// use snapbox::file;
    ///
    /// let assert = Command::new(cargo_bin("snap-fixture"))
    ///     .env("stdout", "hello")
    ///     .env("stderr", "world")
    ///     .assert()
    ///     .stderr_eq(file!["stderr.log"]);
    /// ```
    #[track_caller]
    pub fn stderr_eq(self, expected: impl IntoData) -> Self {
        let expected = expected.into_data();
        self.stderr_eq_inner(expected)
    }

    #[track_caller]
    #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stderr_eq`")]
    pub fn stderr_eq_(self, expected: impl IntoData) -> Self {
        self.stderr_eq(expected)
    }

    #[track_caller]
    fn stderr_eq_inner(self, expected: crate::Data) -> Self {
        let actual = self.output.stderr.as_slice().into_data();
        if let Err(err) = self.config.try_eq(Some(&"stderr"), actual, expected) {
            err.panic();
        }

        self
    }

    fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
        if !self.output.stdout.is_empty() {
            writeln!(writer, "stdout:")?;
            writeln!(writer, "```")?;
            writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
            writeln!(writer, "```")?;
        }
        Ok(())
    }

    fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
        if !self.output.stderr.is_empty() {
            writeln!(writer, "stderr:")?;
            writeln!(writer, "```")?;
            writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
            writeln!(writer, "```")?;
        }
        Ok(())
    }
}

/// Converts an [`std::process::ExitStatus`]  to a human-readable value
#[cfg(not(feature = "cmd"))]
pub fn display_exit_status(status: std::process::ExitStatus) -> String {
    basic_exit_status(status)
}

/// Converts an [`std::process::ExitStatus`]  to a human-readable value
#[cfg(feature = "cmd")]
pub fn display_exit_status(status: std::process::ExitStatus) -> String {
    #[cfg(unix)]
    fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
        use std::os::unix::process::ExitStatusExt;

        let signal = status.signal()?;
        let name = match signal as libc::c_int {
            libc::SIGABRT => ", SIGABRT: process abort signal",
            libc::SIGALRM => ", SIGALRM: alarm clock",
            libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
            libc::SIGHUP => ", SIGHUP: hangup",
            libc::SIGILL => ", SIGILL: illegal instruction",
            libc::SIGINT => ", SIGINT: terminal interrupt signal",
            libc::SIGKILL => ", SIGKILL: kill",
            libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
            libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
            libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
            libc::SIGTERM => ", SIGTERM: termination signal",
            libc::SIGBUS => ", SIGBUS: access to undefined memory",
            #[cfg(not(target_os = "haiku"))]
            libc::SIGSYS => ", SIGSYS: bad system call",
            libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
            _ => "",
        };
        Some(format!("signal: {signal}{name}"))
    }

    #[cfg(windows)]
    fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
        use windows_sys::Win32::Foundation::*;

        let extra = match status.code().unwrap() as NTSTATUS {
            STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
            STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
            STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
            STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
            STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
            STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
            STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
            STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
            STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
            STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
            STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
            STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
            STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
            STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
            STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
            STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
            STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
            STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
            STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
            STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
            STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
            STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
            STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
            STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
            STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
            STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
            STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
            STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
            STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
            STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
            STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
            _ => return None,
        };
        Some(extra.to_owned())
    }

    if let Some(extra) = detailed_exit_status(status) {
        format!("{} ({})", basic_exit_status(status), extra)
    } else {
        basic_exit_status(status)
    }
}

fn basic_exit_status(status: std::process::ExitStatus) -> String {
    if let Some(code) = status.code() {
        code.to_string()
    } else {
        "interrupted".to_owned()
    }
}

#[cfg(feature = "cmd")]
fn wait(
    mut child: std::process::Child,
    timeout: Option<std::time::Duration>,
) -> std::io::Result<std::process::ExitStatus> {
    if let Some(timeout) = timeout {
        wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
            .transpose()
            .unwrap_or_else(|| {
                let _ = child.kill();
                child.wait()
            })
    } else {
        child.wait()
    }
}

#[cfg(not(feature = "cmd"))]
fn wait(
    mut child: std::process::Child,
    _timeout: Option<std::time::Duration>,
) -> std::io::Result<std::process::ExitStatus> {
    child.wait()
}

#[doc(inline)]
pub use crate::cargo_bin;

/// Look up the path to a cargo-built binary within an integration test
///
/// Cargo support:
/// - `>1.94`: works
/// - `>=1.91,<=1.93`: works with default `build-dir`
/// - `<=1.92`: works
///
/// # Panic
///
/// Panics if no binary is found
pub fn cargo_bin(name: &str) -> std::path::PathBuf {
    cargo_bin_opt(name).unwrap_or_else(|| missing_cargo_bin(name))
}

/// Look up the path to a cargo-built binary within an integration test
///
/// Returns `None` if the binary doesn't exist
///
/// Cargo support:
/// - `>1.94`: works
/// - `>=1.91,<=1.93`: works with default `build-dir`
/// - `<=1.92`: works
pub fn cargo_bin_opt(name: &str) -> Option<std::path::PathBuf> {
    let env_var = format!("{CARGO_BIN_EXE_}{name}");
    std::env::var_os(env_var)
        .map(|p| p.into())
        .or_else(|| legacy_cargo_bin(name))
}

/// Return all the name and path for all binaries built by Cargo
///
/// Cargo support:
/// - `>1.94`: works
pub fn cargo_bins() -> impl Iterator<Item = (String, std::path::PathBuf)> {
    std::env::vars_os()
        .filter_map(|(k, v)| {
            k.into_string()
                .ok()
                .map(|k| (k, std::path::PathBuf::from(v)))
        })
        .filter_map(|(k, v)| k.strip_prefix(CARGO_BIN_EXE_).map(|s| (s.to_owned(), v)))
}

const CARGO_BIN_EXE_: &str = "CARGO_BIN_EXE_";

fn missing_cargo_bin(name: &str) -> ! {
    let possible_names: Vec<_> = cargo_bins().map(|(k, _)| k).collect();
    if possible_names.is_empty() {
        panic!("`CARGO_BIN_EXE_{name}` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_{name}`")
    } else {
        let mut names = String::new();
        for (i, name) in possible_names.iter().enumerate() {
            use std::fmt::Write as _;
            if i != 0 {
                let _ = write!(&mut names, ", ");
            }
            let _ = write!(&mut names, "\"{name}\"");
        }
        panic!(
            "`CARGO_BIN_EXE_{name}` is unset
help: available binary names are {names}"
        )
    }
}

fn legacy_cargo_bin(name: &str) -> Option<std::path::PathBuf> {
    let target_dir = target_dir()?;
    let bin_path = target_dir.join(format!("{}{}", name, std::env::consts::EXE_SUFFIX));
    if !bin_path.exists() {
        return None;
    }
    Some(bin_path)
}

// Adapted from
// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
fn target_dir() -> Option<std::path::PathBuf> {
    let mut path = std::env::current_exe().ok()?;
    let _test_bin_name = path.pop();
    if path.ends_with("deps") {
        let _deps = path.pop();
    }
    Some(path)
}

#[cfg(feature = "examples")]
pub use examples::{compile_example, compile_examples};

#[cfg(feature = "examples")]
pub(crate) mod examples {
    /// Prepare an example for testing
    ///
    /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings.  It
    /// will match the current target and profile but will not get feature flags.  Pass those arguments
    /// to the compiler via `args`.
    ///
    /// ## Example
    ///
    /// ```rust,no_run
    /// snapbox::cmd::compile_example("snap-example-fixture", []);
    /// ```
    #[cfg(feature = "examples")]
    pub fn compile_example<'a>(
        target_name: &str,
        args: impl IntoIterator<Item = &'a str>,
    ) -> crate::assert::Result<std::path::PathBuf> {
        crate::debug!("Compiling example {}", target_name);
        let messages = escargot::CargoBuild::new()
            .current_target()
            .current_release()
            .example(target_name)
            .args(args)
            .exec()
            .map_err(|e| crate::assert::Error::new(e.to_string()))?;
        for message in messages {
            let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
            let message = message
                .decode()
                .map_err(|e| crate::assert::Error::new(e.to_string()))?;
            crate::debug!("Message: {:?}", message);
            if let Some(bin) = decode_example_message(&message) {
                let (name, bin) = bin?;
                assert_eq!(target_name, name);
                return bin;
            }
        }

        Err(crate::assert::Error::new(format!(
            "Unknown error building example {target_name}"
        )))
    }

    /// Prepare all examples for testing
    ///
    /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings.  It
    /// will match the current target and profile but will not get feature flags.  Pass those arguments
    /// to the compiler via `args`.
    ///
    /// ## Example
    ///
    /// ```rust,no_run
    /// let examples = snapbox::cmd::compile_examples([]).unwrap().collect::<Vec<_>>();
    /// ```
    #[cfg(feature = "examples")]
    pub fn compile_examples<'a>(
        args: impl IntoIterator<Item = &'a str>,
    ) -> crate::assert::Result<
        impl Iterator<Item = (String, crate::assert::Result<std::path::PathBuf>)>,
    > {
        crate::debug!("Compiling examples");
        let mut examples = std::collections::BTreeMap::new();

        let messages = escargot::CargoBuild::new()
            .current_target()
            .current_release()
            .examples()
            .args(args)
            .exec()
            .map_err(|e| crate::assert::Error::new(e.to_string()))?;
        for message in messages {
            let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
            let message = message
                .decode()
                .map_err(|e| crate::assert::Error::new(e.to_string()))?;
            crate::debug!("Message: {:?}", message);
            if let Some(bin) = decode_example_message(&message) {
                let (name, bin) = bin?;
                examples.insert(name.to_owned(), bin);
            }
        }

        Ok(examples.into_iter())
    }

    #[allow(clippy::type_complexity)]
    fn decode_example_message<'m>(
        message: &'m escargot::format::Message<'_>,
    ) -> Option<crate::assert::Result<(&'m str, crate::assert::Result<std::path::PathBuf>)>> {
        match message {
            escargot::format::Message::CompilerMessage(msg) => {
                let level = msg.message.level;
                if level == escargot::format::diagnostic::DiagnosticLevel::Ice
                    || level == escargot::format::diagnostic::DiagnosticLevel::Error
                {
                    let output = msg
                        .message
                        .rendered
                        .as_deref()
                        .unwrap_or_else(|| msg.message.message.as_ref())
                        .to_owned();
                    if is_example_target(&msg.target) {
                        let bin = Err(crate::assert::Error::new(output));
                        Some(Ok((msg.target.name.as_ref(), bin)))
                    } else {
                        Some(Err(crate::assert::Error::new(output)))
                    }
                } else {
                    None
                }
            }
            escargot::format::Message::CompilerArtifact(artifact) => {
                if !artifact.profile.test && is_example_target(&artifact.target) {
                    let path = artifact
                        .executable
                        .clone()
                        .expect("cargo is new enough for this to be present");
                    let bin = Ok(path.into_owned());
                    Some(Ok((artifact.target.name.as_ref(), bin)))
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    fn is_example_target(target: &escargot::format::Target<'_>) -> bool {
        target.crate_types == ["bin"] && target.kind == ["example"]
    }
}

#[test]
#[should_panic = "`CARGO_BIN_EXE_non-existent` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_non-existent`"]
fn cargo_bin_in_unit_test() {
    cargo_bin("non-existent");
}
