//
// Syd: rock-solid application kernel
// src/test/util.rs: Utilities for integration tests
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![allow(clippy::disallowed_methods)]
#![allow(clippy::disallowed_types)]

use std::{
    env,
    error::Error,
    ffi::OsStr,
    fmt,
    fs::{canonicalize, File},
    io::Write,
    net::{Ipv6Addr, SocketAddrV6, TcpListener},
    os::fd::RawFd,
    path::{Path, PathBuf},
    process::{Child, Command, ExitStatus, Output, Stdio},
    time::Duration,
};

use nix::{
    errno::Errno,
    sys::{
        resource::{setrlimit, Resource, RLIM_INFINITY},
        utsname::uname,
    },
};
use once_cell::sync::Lazy;
use syd::config::*;

#[derive(Debug)]
pub struct TestError(pub String);
pub type TestResult = Result<(), TestError>;

impl fmt::Display for TestError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<E: Error> From<E> for TestError {
    fn from(err: E) -> Self {
        TestError(err.to_string())
    }
}

#[macro_export]
macro_rules! assert {
    ($cond:expr) => {
        if !$cond {
            return Err(TestError(format!("Assertion failed: {}", stringify!($cond))));
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if !$cond {
            return Err(TestError(format!("Assertion failed: {}: {}", stringify!($cond), format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! assert_eq {
    ($left:expr, $right:expr) => {
        if $left != $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
        }
    };
    ($left:expr, $right:expr, $($arg:tt)*) => {
        if $left != $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! assert_ne {
    ($left:expr, $right:expr) => {
        if $left == $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
        }
    };
    ($left:expr, $right:expr, $($arg:tt)*) => {
        if $left == $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! fixup {
    ($cond:expr) => {
        if $cond {
            return Err(TestError(format!("Known issue fixed in {}:{}", file!(), line!())));
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if $cond {
            return Err(TestError(format!("Known issue fixed in {}:{}: {}", file!(), line!(), format_args!($($arg)*))));
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
        }
    };
}

#[macro_export]
macro_rules! ignore {
    ($cond:expr) => {
        if $cond {
            eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if $cond {
            eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
        }
    };
}

#[macro_export]
macro_rules! assert_status_aborted {
    ($status:expr) => {{
        const XABRT: i32 = 128 + libc::SIGABRT;
        const XBUS: i32 = 128 + libc::SIGBUS;
        const XILL: i32 = 128 + libc::SIGILL;
        const XSEGV: i32 = 128 + libc::SIGSEGV;
        assert_status_code_matches!($status, XABRT | XBUS | XILL | XSEGV);
    }};
}

#[macro_export]
macro_rules! assert_status_panicked {
    ($status:expr) => {{
        // If the main thread panics it will terminate all your threads
        // and end your program with code 101.
        // See: https://doc.rust-lang.org/std/macro.panic.html
        assert_status_code!($status, 101);
    }};
}

#[macro_export]
macro_rules! assert_status_code_matches {
    ($status:expr, $($pattern:tt)+) => {{
        let code = $status.code().unwrap_or(127);
        assert!(
            matches!(code, $($pattern)+),
            "code: {code} status: {:?}",
            $status
        );
    }};
}

#[macro_export]
macro_rules! assert_status_code {
    ($status:expr, $expected:expr) => {
        let code = $status.code().unwrap_or(127);
        assert_eq!(code, $expected, "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_ok {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        assert!($status.success(), "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_not_ok {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        assert!(!$status.success(), "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_bad_message {
    ($status:expr) => {
        assert_status_code!($status, libc::EBADMSG);
    };
}

#[macro_export]
macro_rules! assert_status_denied {
    ($status:expr) => {
        assert_status_code!($status, libc::EACCES);
    };
}

#[macro_export]
macro_rules! assert_status_hidden {
    ($status:expr) => {
        assert_status_code!($status, libc::ENOENT);
    };
}

#[macro_export]
macro_rules! assert_status_invalid {
    ($status:expr) => {
        assert_status_code!($status, libc::EINVAL);
    };
}

#[macro_export]
macro_rules! assert_status_interrupted {
    ($status:expr) => {
        assert_status_code!($status, libc::EINTR);
    };
}

#[macro_export]
macro_rules! assert_status_killed {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        assert_eq!(
            code,
            128 + libc::SIGKILL,
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! assert_status_sigsys {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        assert_eq!(code, 128 + libc::SIGSYS, "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_signaled {
    ($status:expr, $signal:expr) => {
        let signal = $status.signal().unwrap_or(127);
        assert_eq!(signal, $signal, "signal:{signal} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_not_supported {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        assert!(
            matches!(code, libc::EAFNOSUPPORT | nix::libc::ENOTSUP),
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! skip_unless_available {
    ($($program:expr),* $(,)?) => {{
        let mut all_available = true;
        $(
            if !is_program_available($program) {
                eprintln!("Test requires {}, skipping!", $program);
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                all_available = false;
            }
        )*

        if !all_available {
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_32bin_64host {
    () => {{
        if check_32bin_64host() {
            eprintln!("Binary/Host mismatch, cannot run test, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_root {
    () => {{
        if Uid::effective().is_root() {
            eprintln!("Test requires to run as non-root user, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("Thanks for not running this test as root!");
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_kernel_crypto_is_supported {
    () => {{
        let key = syd::hash::Key::random().unwrap();
        match syd::hash::aes_ctr_setup(&key) {
            Ok(fd) => drop(fd),
            Err(nix::errno::Errno::EAFNOSUPPORT) => {
                eprintln!("Test requires Linux Kernel Cryptography API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(format!(
                    "Failed to test for Linux Kernel Cryptography API: {errno}"
                )));
            }
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_xattrs_are_supported {
    () => {{
        let fd = match nix::fcntl::open(
            "xattr.test",
            nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CREAT | nix::fcntl::OFlag::O_EXCL,
            nix::sys::stat::Mode::from_bits_truncate(0o600),
        ) {
            Ok(fd) => fd,
            Err(errno) => return Err(TestError(format!("Failed to open xattr.test: {errno}"))),
        };

        match syd::fs::fsetxattr(&fd, "user.syd.crypt.api", b"3", libc::XATTR_CREATE) {
            Ok(_) => {
                let _ = nix::unistd::close(fd);
            }
            Err(nix::errno::Errno::EOPNOTSUPP) => {
                let _ = nix::unistd::close(fd);
                eprintln!("Test requires extended attributes support, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                let _ = nix::unistd::close(fd);
                return Err(TestError(format!(
                    "Failed to test for extended attributes support: {errno}"
                )));
            }
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_unshare {
    () => {{
        match check_unshare() {
            Some(false) => {
                eprintln!("Test requires Linux namespaces, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            None => {
                return Err(TestError(
                    "Failed to test for Linux namespaces!".to_string(),
                ));
            }
            _ => {}
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_coredumps {
    () => {{
        if let Err(errno) = enable_coredumps() {
            eprintln!("Failed to enable coredumps: {errno}!");
            eprintln!("Skipping test!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_exists {
    ($path:expr) => {{
        if !std::path::Path::new($path).exists() {
            eprintln!("Test requires the path \"{}\" to exist, skipping!", $path);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_bitness {
    ($bitness:expr) => {{
        if !cfg!(target_pointer_width = $bitness) {
            eprintln!("Test requires a {}-bit syd, skipping!", $bitness);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_feature {
    ($feature:expr) => {{
        if !cfg!(feature = $feature) {
            eprintln!(
                "Test requires syd built with {} feature, skipping!",
                $feature
            );
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_cap {
    ($cap:expr) => {{
        use std::str::FromStr;
        let cap = syd::caps::Capability::from_str(&syd::caps::to_canonical($cap)).expect("cap2str");
        if !syd::caps::has_cap(None, syd::caps::CapSet::Permitted, cap).expect("syd::caps::has_cap")
        {
            eprintln!("Test requires {cap} capability, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("Capability {cap} set, proceeding with test.");
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_landlock_abi_supported {
    ($abi:expr) => {{
        use std::process::Command;

        // Run `syd-lock -A` to get the ABI version as exit code.
        let status = Command::new(&*SYD_LOCK)
            .arg("-A")
            .status()
            .expect("Failed to run syd-lock -A");

        // SAFETY: We expect `syd-lock -A` to exit with a code.
        let exit_code = status
            .code()
            .expect("Failed to get exit code from syd-lock -A");

        if exit_code < $abi {
            eprintln!("Test requires Landlock ABI version {}, skipping!", $abi);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!(
                "Landlock ABI {} is supported, proceeding with test...",
                $abi
            );
        }
    }};
}

#[macro_export]
macro_rules! skip_if_strace {
    () => {{
        if std::env::var("SYD_TEST_STRACE").is_ok() && std::env::var("SYD_TEST_FORCE").is_err() {
            eprintln!("Test does not work correctly under strace, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_pty {
    () => {{
        use std::process::Command;

        skip_unless_available!("python");

        // Run python to test for support.
        //
        // TODO: Make syd-tty do this.
        let status = Command::new("python")
            .arg("-c")
            .arg("import os; os.openpty()")
            .status()
            .expect("execute python");

        if status.code().unwrap_or(127) != 0 {
            eprintln!("Test requires PTY access, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("PTY access is available, proceeding with test...");
        }
    }};
}

/// Holds a `String` to run `syd`.
/// Honours CARGO_BIN_EXE_syd environment variable.
pub static SYD: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd").unwrap_or("syd".to_string()));

/// Holds a `String` to run `syd-aux`.
/// Honours CARGO_BIN_EXE_syd-aux environment variable.
pub static SYD_AUX: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-aux").unwrap_or("syd-aux".to_string()));

/// Holds a `String` to run `syd-bit`.
/// Honours CARGO_BIN_EXE_syd-aux environment variable.
pub static SYD_BIT: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-bit").unwrap_or("syd-bit".to_string()));

/// Holds a `String` to run `syd-cap`.
/// Honours CARGO_BIN_EXE_syd-cap environment variable.
pub static SYD_CAP: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-cap").unwrap_or("syd-cap".to_string()));

/// Holds a `String` to run `syd-dns`.
/// Honours CARGO_BIN_EXE_syd-dns environment variable.
pub static SYD_DNS: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-dns").unwrap_or("syd-dns".to_string()));

/// Holds a `String` to run `syd-env`.
/// Honours CARGO_BIN_EXE_syd-env environment variable.
pub static SYD_ENV: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-env").unwrap_or("syd-env".to_string()));

/// Holds a `String` to run `syd-exec`.
/// Honours CARGO_BIN_EXE_syd-exec environment variable.
pub static SYD_EXEC: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-exec").unwrap_or("syd-exec".to_string()));

/// Holds a `String` to run `syd-aes`.
/// Honours CARGO_BIN_EXE_syd-aes environment variable.
pub static SYD_AES: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-aes").unwrap_or("syd-aes".to_string()));

/// Holds a `String` to run `syd-key`.
/// Honours CARGO_BIN_EXE_syd-key environment variable.
pub static SYD_KEY: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-key").unwrap_or("syd-key".to_string()));

/// Holds a `String` to run `syd-elf`.
/// Honours CARGO_BIN_EXE_syd-elf environment variable.
pub static SYD_ELF: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-elf").unwrap_or("syd-elf".to_string()));

/// Holds a `String` to run `syd-cpu`.
/// Honours CARGO_BIN_EXE_syd-cpu environment variable.
pub static SYD_CPU: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-cpu").unwrap_or("syd-cpu".to_string()));

/// Holds a `String` to run `syd-hex`.
/// Honours CARGO_BIN_EXE_syd-hex environment variable.
pub static SYD_HEX: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-hex").unwrap_or("syd-hex".to_string()));

/// Holds a `String` to run `syd-info`.
/// Honours CARGO_BIN_EXE_syd-info environment variable.
pub static SYD_INFO: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-info").unwrap_or("syd-info".to_string()));

/// Holds a `String` to run `syd-lock`.
/// Honours CARGO_BIN_EXE_syd-lock environment variable.
pub static SYD_LOCK: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-lock").unwrap_or("syd-lock".to_string()));

/// Holds a `String` to run `syd-pds`.
/// Honours CARGO_BIN_EXE_syd-pds environment variable.
pub static SYD_PDS: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-pds").unwrap_or("syd-pds".to_string()));

/// Holds a `String` to run `syd-size`.
/// Honours CARGO_BIN_EXE_syd-size environment variable.
pub static SYD_SIZE: Lazy<String> =
    Lazy::new(|| env::var("CARGO_BIN_EXE_syd-size").unwrap_or("syd-size".to_string()));

/// Holds a `String` to run `syd-test-do`.
/// Honours CARGO_BIN_EXE_syd-test-do environment variable.
/// This path is a canonicalized for ease of use in sandboxing tests.
pub static SYD_DO: Lazy<String> = Lazy::new(|| match env::var("CARGO_BIN_EXE_syd-test-do") {
    Ok(var) => Path::new(&var)
        .canonicalize()
        .expect("CARGO_BIN_EXE_syd-test-do")
        .to_string_lossy()
        .into_owned(),
    Err(_) => which(if env::var("SYD_TEST_32").is_ok() {
        "syd-test-do32"
    } else {
        "syd-test-do"
    })
    .expect("syd-test-do"),
});

/// A boolean which specifies if we're running under SourceHut CI.
pub static CI_BUILD: Lazy<bool> = Lazy::new(|| env::var("JOB_ID").ok().is_some());

/// A boolean which specifies if we're running under GitLab CI.
pub static GL_BUILD: Lazy<bool> = Lazy::new(|| env::var("CI_PROJECT_ID").ok().is_some());

pub struct Syd(Command);

impl Syd {
    pub fn new(cmd: &str) -> Self {
        Syd(Command::new(cmd))
    }

    pub fn c<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.0.arg(format!("-c{}", arg.to_string()));
        self
    }

    pub fn m<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.0.arg(format!("-m{}", arg.to_string()));
        self
    }

    pub fn p<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.0.arg(format!("-p{}", arg.to_string()));
        self
    }

    #[allow(non_snake_case)]
    pub fn P<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.0.arg(format!("-P{}", arg.to_string()));
        self
    }

    pub fn log(&mut self, value: &str) -> &mut Self {
        self.0
            .env(ENV_LOG, env::var(ENV_LOG).unwrap_or(value.to_string()));
        self
    }

    pub fn log_fd(&mut self, fd: RawFd) -> &mut Self {
        self.0.env(ENV_LOG_FD, fd.to_string());
        self
    }

    pub fn do_<I, S, V>(&mut self, value: V, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.0.args(["--", &SYD_DO]);
        self.0.args(args);
        self.do__(value)
    }

    pub fn do__<V>(&mut self, value: V) -> &mut Self
    where
        V: AsRef<OsStr>,
    {
        self.0.env("SYD_TEST_DO", value);
        self
    }

    pub fn argv<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        self.0.arg("--");
        self.0.args(args);
        self
    }

    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
        self.0.arg(arg);
        self
    }

    pub fn args<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        self.0.args(args);
        self
    }

    /*
    pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
        self.0.stdin(cfg);
        self
    }
    */

    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
        self.0.stdout(cfg);
        self
    }

    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
        self.0.stderr(cfg);
        self
    }

    pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.0.env(key, value);
        self
    }

    pub fn env_remove<K>(&mut self, key: K) -> &mut Self
    where
        K: AsRef<OsStr>,
    {
        self.0.env_remove(key);
        self
    }

    pub fn spawn(&mut self) -> std::io::Result<Child> {
        eprintln!("\x1b[93m+ {:?}\x1b[0m", self.0);
        self.0.spawn()
    }

    pub fn status(&mut self) -> std::io::Result<ExitStatus> {
        eprintln!("\x1b[93m+ {:?}\x1b[0m", self.0);
        self.0.status()
    }

    pub fn output(&mut self) -> std::io::Result<Output> {
        eprintln!("\x1b[93m+ {:?}\x1b[0m", self.0);

        // With `output` stderr defaults to `piped`.
        self.0.stderr(Stdio::inherit());

        self.0.output()
    }
}

/// Returns a `Command` to run `syd`.
/// Honours CARGO_BIN_EXE_syd environment variable.
pub fn syd() -> Syd {
    static USE_PERF: Lazy<bool> = Lazy::new(|| env::var_os("SYD_TEST_PERF").is_some());
    static USE_TRACE: Lazy<bool> = Lazy::new(|| env::var_os("SYD_TEST_TRACE").is_some());
    static USE_STRACE: Lazy<bool> = Lazy::new(|| env::var_os("SYD_TEST_STRACE").is_some());
    static USE_VALGRIND: Lazy<bool> = Lazy::new(|| env::var_os("SYD_TEST_VALGRIND").is_some());
    static USE_HELGRIND: Lazy<bool> = Lazy::new(|| env::var_os("SYD_TEST_HELGRIND").is_some());
    let mut cmd = Syd::new("timeout");
    if check_timeout_foreground() {
        cmd.arg("--foreground");
        cmd.arg("--preserve-status");
        cmd.arg("--verbose");
    }
    cmd.arg("-sKILL");
    cmd.arg(env::var("SYD_TEST_TIMEOUT").unwrap_or("10m".to_string()));
    if *USE_PERF {
        cmd.arg("perf");
        cmd.arg("record");
        cmd.arg("-F99");
        cmd.arg("--call-graph=dwarf");
        cmd.arg("-o/tmp/syd-perf.data"); // FIXME
        cmd.arg("--");
    } else if *USE_STRACE {
        cmd.arg("strace");
        cmd.arg("-f");

        if env::var_os("SYD_TEST_STRACE_VERBOSE").is_none() {
            cmd.arg("-s256");
            cmd.arg("-e!read,readv,write,writev");
        } else {
            cmd.arg("-s4096");
        }

        let arg = env::var_os("SYD_TEST_STRACE").unwrap();
        if !arg.is_empty() {
            cmd.arg(arg);
        }

        cmd.arg("--");
    } else if *USE_VALGRIND {
        cmd.arg("valgrind");
        cmd.arg("--leak-check=yes");
        cmd.arg("--track-origins=yes");
        cmd.arg("--track-fds=yes");
        cmd.arg("--trace-children=no");
        cmd.arg("--");
    } else if *USE_HELGRIND {
        cmd.arg("valgrind");
        cmd.arg("--tool=helgrind");
        cmd.arg("--");
    }
    cmd.arg(&*SYD);
    // To get meaningful panics:
    // 1. Set quick boot to on.
    // 2. Set as dumpable.
    cmd.arg("-q");
    cmd.m("trace/allow_unsafe_dumpable:1");
    // Set logging level to warning.
    // Allow user to override.
    cmd.env(ENV_LOG, env::var(ENV_LOG).unwrap_or("warn".to_string()));
    // Set log/verbose to false to avoid noisy test logs.
    cmd.m("log/verbose:0");
    // Quiet TTY output to avoid noisy test logs.
    // Unless otherwise specified.
    if env::var_os(ENV_FORCE_TTY).is_none() {
        cmd.env(ENV_QUIET_TTY, "YesPlease");
    }
    // Allow coredumps and non-PIE.
    cmd.m("trace/allow_unsafe_prlimit:1"); // Allow coredumps.
    cmd.m("trace/allow_unsafe_nopie:1"); // Allow non-PIE.
    if *USE_STRACE || *USE_TRACE {
        cmd.m("trace/allow_unsafe_ptrace:1"); // Disable ptrace.
    }
    cmd
}

/// 0. Check if there's a binary/host mismatch.
/// 1. Check if a program exists in PATH
pub fn is_program_available(command: &str) -> bool {
    if check_32bin_64host() {
        eprintln!("Binary/Host mismatch, cannot use program {command}!");
        return false;
    }

    // Check if the command exists in PATH
    Command::new("which")
        .stdout(Stdio::null())
        .arg(command)
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Resembles the `which` command, finds a program in PATH.
pub fn which(command: &str) -> Result<String, Errno> {
    let out = Command::new("which")
        .arg(command)
        .output()
        .expect("execute which")
        .stdout;
    if out.is_empty() {
        return Err(Errno::ENOENT);
    }

    let bin = String::from_utf8_lossy(&out);
    let bin = bin.trim();
    Ok(canonicalize(bin)
        .map_err(|_| Errno::last())?
        .to_string_lossy()
        .into_owned())
}

/// Check if IPv6 is supported
pub fn check_ipv6() -> bool {
    // Preliminary check for IPv6 availability
    let test_sock = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0);
    if let Err(error) = TcpListener::bind(test_sock) {
        eprintln!("IPv6 is not available on this system. Skipping test: {error}");
        false
    } else {
        true
    }
}

/// Check if namespaces are supported.
/// Returns None if syd process was terminated by a signal.
pub fn check_unshare() -> Option<bool> {
    syd()
        .args(["-poff", "-pD", "-pcontainer", "true"])
        .status()
        .map(|stat| stat.code())
        .ok()?
        .map(|code| code == 0)
}

/// Checks if the C compiler allows nested routines.
/// Returns `true` if supported, `false` otherwise.
/// If successful, creates the executable `nested` in CWD.
/// See: https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html
pub fn check_nested_routines() -> bool {
    let c_code = r#"
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int x = atoi(argv[1]);
    int nested()
    {
        return x * x;
    };

    // Use pointer indirection so compiler cannot unnest.
    int (*fp)() = nested;

    // Yolo!
    return fp();
}
"#;
    if let Ok(mut file) = File::create("nested.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler
    let compile_status = Command::new("cc")
        .arg("-Wall")
        .arg("nested.c")
        .arg("-o")
        .arg("nested")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Checks if the C compiler allows self-modifying code with mprotect.
/// Returns `true` if supported, `false` otherwise.
/// Only works on x86-64, always returns `false` on other architectures.
/// If successful, creates the executable `selfmod` in CWD.
/// See: https://shanetully.com/2013/12/writing-a-self-mutating-x86_64-c-program/
pub fn check_self_modifying_mp() -> bool {
    if !cfg!(target_arch = "x86_64") {
        return false;
    }

    let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

void foo(void);
int change_page_permissions_of_address(void *addr);

int main(void) {
    void *foo_addr = (void*)foo;

    // Change the permissions of the page that contains foo() to read,
    // write, and execute. This assumes that foo() is fully contained
    // by a single page.
    if(change_page_permissions_of_address(foo_addr) == -1) {
        int save_errno = errno;
        fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
        return save_errno;
    }

    // Call the unmodified foo()
    puts("Calling foo...");
    foo();

    // Change the immediate value in the addl instruction in foo() to 42
    unsigned char *instruction = (unsigned char*)foo_addr + 18;
    *instruction = 0x2A;

    // Call the modified foo()
    puts("Calling foo...");
    foo();

    return 0;
}

void foo(void) {
    int i=0;
    i++;
    printf("i: %d\n", i);
}

int change_page_permissions_of_address(void *addr) {
    // Move the pointer to the page boundary
    int page_size = getpagesize();
    addr -= (unsigned long)addr % page_size;

    if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
        return -1;
    }

    return 0;
}
"#;
    if let Ok(mut file) = File::create("selfmod.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler.
    let compile_status = Command::new("cc")
        .arg("-std=c99")
        .arg("-D_BSD_SOURCE")
        .arg("-Wall")
        .arg("selfmod.c")
        .arg("-o")
        .arg("selfmod")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise.
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Checks if the C compiler allows self-modifying code with executable stack.
/// Returns `true` if supported, `false` otherwise.
/// Only works on x86-64, always returns `false` on other architectures.
/// If successful, creates the executable `selfmod` in CWD.
/// See: https://shanetully.com/2013/12/writing-a-self-mutating-x86_64-c-program/
pub fn check_self_modifying_xs() -> bool {
    if !cfg!(target_arch = "x86_64") {
        return false;
    }

    let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

void foo(void);
int change_page_permissions_of_address(void *addr);

int main(void) {
    void *foo_addr = (void*)foo;

    // Change the permissions of the page that contains foo() to read,
    // write, and execute. This assumes that foo() is fully contained
    // by a single page.
    if(change_page_permissions_of_address(foo_addr) == -1) {
        int save_errno = errno;
        fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
        return save_errno;
    }

    // Call the unmodified foo()
    puts("Calling foo...");
    foo();

    // Change the immediate value in the addl instruction in foo() to 42
    unsigned char *instruction = (unsigned char*)foo_addr + 18;
    *instruction = 0x2A;

    // Call the modified foo()
    puts("Calling foo...");
    foo();

    return 0;
}

void foo(void) {
    int i=0;
    i++;
    printf("i: %d\n", i);
}

int change_page_permissions_of_address(void *addr) {
    // Move the pointer to the page boundary
    int page_size = getpagesize();
    addr -= (unsigned long)addr % page_size;

    if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
        return -1;
    }

    return 0;
}
"#;
    if let Ok(mut file) = File::create("selfmod.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler.
    let compile_status = Command::new("cc")
        .arg("-std=c99")
        .arg("-D_BSD_SOURCE")
        .arg("-zexecstack")
        .arg("-Wall")
        .arg("selfmod.c")
        .arg("-o")
        .arg("selfmod")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise.
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Format a `Duration` into a human readable `String`.
pub fn format_duration(d: Duration) -> String {
    let total_seconds = d.as_secs();
    let hours = total_seconds / 3600;
    let minutes = (total_seconds % 3600) / 60;
    let seconds = total_seconds % 60;

    format!("{}h {}m {}s", hours, minutes, seconds)
}

/// Checks if the current running binary is 32-bit and the host system is 64-bit.
pub fn check_32bin_64host() -> bool {
    // Check if the current binary is 32-bit using a compile-time constant.
    // This constant is set by the Cargo build script based on the target architecture.
    #[cfg(target_pointer_width = "32")]
    let is_binary_32bit = true;
    #[cfg(not(target_pointer_width = "32"))]
    let is_binary_32bit = false;

    // Use uname to check if the host is 64-bit.
    let arch = uname()
        .expect("uname")
        .machine()
        .to_string_lossy()
        .to_string();

    if is_binary_32bit && arch.contains("64") {
        eprintln!("32->64: Running 32bit on {arch}!");
        true
    } else {
        false
    }
}

/// Enable coredumps.
pub fn enable_coredumps() -> Result<(), Errno> {
    // Set both the soft and hard limits
    setrlimit(Resource::RLIMIT_CORE, RLIM_INFINITY, RLIM_INFINITY)
}

/// Check if timeout --foreground is supported.
pub fn check_timeout_foreground() -> bool {
    Command::new("timeout")
        .arg("--foreground")
        .arg("-sKILL")
        .arg("60s")
        .arg("true")
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Retrieves the current directory or its basename as a `PathBuf`, based on the `base` parameter.
pub fn current_dir(base: bool) -> std::io::Result<PathBuf> {
    // Get the current working directory
    let current_dir = env::current_dir()?;

    if base {
        // Extract the basename
        let basename = current_dir
            .file_name()
            .ok_or(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Failed to get the basename",
            ))
            .map(PathBuf::from)?;

        Ok(basename)
    } else {
        // Return the full path as PathBuf
        current_dir.canonicalize()
    }
}

/// Shuffles a vector using the Fisher-Yates algorithm,
/// utilizing random numbers obtained via `libc::rand`.
///
/// # Arguments
/// * `vec` - A mutable reference to the vector to shuffle.
pub fn shuffle_vec<T>(vec: &mut [T]) {
    let len = vec.len();
    for i in 0..len {
        let r = unsafe { libc::rand() } as usize;
        let j = r % (len - i) + i;
        vec.swap(i, j); // perform the swap
    }
}
