// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore readelf

use uutests::util::TestScenario;

#[cfg(unix)]
use std::os::unix::fs::symlink as symlink_file;

use std::env;
pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils");

// Set the environment variable for any tests

// Use the ctor attribute to run this function before any tests
#[ctor::ctor(unsafe)]
fn init() {
    unsafe {
        env::set_var("UUTESTS_BINARY_PATH", TESTS_BINARY);
    }
    // Print for debugging
    eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}");
}

#[test]
#[cfg(all(
    feature = "env",
    any(target_os = "linux", target_os = "android"),
    not(target_env = "musl")
))]
fn binary_name_protection() {
    let ts = TestScenario::new("env");
    let bin = ts.bin_path.clone();
    ts.ucmd()
        .arg("-a")
        .arg("hijacked")
        .arg(&bin)
        .arg("--version")
        .succeeds()
        .stdout_contains("coreutils");
}

#[test]
fn test_coreutils_help_ignore_args() {
    let scenario = TestScenario::new("help_ignoring_args");
    let output = std::process::Command::new(&scenario.bin_path)
        .arg("--help")
        .arg("---")
        .output()
        .unwrap();

    assert_eq!(output.status.code(), Some(0));
}

#[test]
#[cfg(feature = "ls")]
fn execution_phrase_double() {
    use std::process::Command;

    let scenario = TestScenario::new("ls");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }
    let output = Command::new(&scenario.bin_path)
        .arg("ls")
        .arg("--some-invalid-arg")
        .env("LANG", "en_US.UTF-8")
        .output()
        .unwrap();
    assert!(
        String::from_utf8(output.stderr)
            .unwrap()
            .contains(&"Usage: ls".to_string())
    );
}

#[test]
#[ignore = "Test assumes error upon non-UTF8"]
#[cfg(feature = "sort")]
fn util_name_double() {
    use std::{
        io::Write,
        process::{Command, Stdio},
    };

    let scenario = TestScenario::new("sort");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }
    let mut child = Command::new(&scenario.bin_path)
        .arg("sort")
        .stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    // input invalid utf8 to cause an error
    child.stdin.take().unwrap().write_all(&[255]).unwrap();
    let output = child.wait_with_output().unwrap();
    assert!(String::from_utf8(output.stderr).unwrap().contains("sort: "));
}

#[test]
#[ignore = "Test assumes error upon non-UTF8"]
#[cfg(feature = "sort")]
#[cfg(unix)]
fn util_name_single() {
    use std::{
        io::Write,
        process::{Command, Stdio},
    };

    let scenario = TestScenario::new("sort");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }

    symlink_file(&scenario.bin_path, scenario.fixtures.plus("uu-sort")).unwrap();
    let mut child = Command::new(scenario.fixtures.plus("uu-sort"))
        .stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    // input invalid utf8 to cause an error
    child.stdin.take().unwrap().write_all(&[255]).unwrap();
    let output = child.wait_with_output().unwrap();
    assert!(String::from_utf8(output.stderr).unwrap().contains(&format!(
        "{}: ",
        scenario.fixtures.plus("uu-sort").display()
    )));
}

#[test]
#[cfg(unix)]
fn util_invalid_name_help() {
    use std::process::Command;

    let scenario = TestScenario::new("invalid_name");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }
    symlink_file(&scenario.bin_path, scenario.fixtures.plus("invalid_name")).unwrap();
    let code = Command::new(scenario.fixtures.plus("invalid_name"))
        .arg("--help")
        .status()
        .unwrap()
        .code();
    assert_eq!(code, Some(1));
}

#[test]
// The exact set of permitted filenames depends on many factors. Non-UTF-8 strings
// work on very few platforms, but linux works, especially because it also increases
// the likelihood that a filesystem is being used that supports non-UTF-8 filenames.
#[cfg(target_os = "linux")]
fn util_non_utf8_name_help() {
    // Make sure we don't crash even if the util name is invalid UTF-8.
    use std::{
        ffi::OsStr,
        os::unix::ffi::OsStrExt,
        process::{Command, Stdio},
    };

    let scenario = TestScenario::new("invalid_name");
    let non_utf8_path = scenario.fixtures.plus(OsStr::from_bytes(b"\xff"));
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }

    symlink_file(&scenario.bin_path, &non_utf8_path).unwrap();
    let child = Command::new(&non_utf8_path)
        .arg("--help")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    let output = child.wait_with_output().unwrap();
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(output.stderr, b"");
    let output_str = String::from_utf8(output.stdout).unwrap();
    assert!(output_str.contains("(multi-call binary)"), "{output_str:?}");
    assert!(
        output_str.contains("Usage: <unknown binary name> [function "),
        "{output_str:?}"
    );
}

#[test]
#[cfg(unix)]
fn util_invalid_name_invalid_command() {
    use std::process::Command;

    let scenario = TestScenario::new("invalid_name");
    symlink_file(&scenario.bin_path, scenario.fixtures.plus("invalid_name")).unwrap();
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }

    let code = Command::new(scenario.fixtures.plus("invalid_name"))
        .status()
        .unwrap()
        .code();
    assert_eq!(code, Some(1));
}

#[test]
fn util_version() {
    use std::process::{Command, Stdio};

    let scenario = TestScenario::new("--version");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }
    for arg in ["-V", "--version"] {
        let child = Command::new(&scenario.bin_path)
            .arg(arg)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        let output = child.wait_with_output().unwrap();
        assert_eq!(output.status.code(), Some(0));
        assert_eq!(output.stderr, b"");
        let output_str = String::from_utf8(output.stdout).unwrap();
        let ver = env::var("CARGO_PKG_VERSION").unwrap();
        assert_eq!(format!("coreutils {ver} (multi-call binary)\n"), output_str);
    }
}

#[test]
#[cfg(target_env = "musl")]
fn test_musl_no_dynamic_deps() {
    use std::process::Command;

    let scenario = TestScenario::new("test_musl_no_dynamic_deps");
    if !scenario.bin_path.exists() {
        println!("Skipping test: Binary not found at {:?}", scenario.bin_path);
        return;
    }

    let output = Command::new("readelf")
        .arg("-d")
        .arg(&scenario.bin_path)
        .output()
        .expect("Failed to run readelf");

    let stdout = String::from_utf8_lossy(&output.stdout);

    // Static binaries should have no NEEDED entries (dynamic library dependencies)
    assert!(
        !stdout.contains("NEEDED"),
        "Found dynamic dependencies in musl binary:\n{}",
        stdout
    );
}

#[test]
fn test_sorted_utils() {
    let s = TestScenario::new("list_sorted");
    let out = String::from_utf8(
        std::process::Command::new(&s.bin_path)
            .arg("--list")
            .output()
            .unwrap()
            .stdout,
    )
    .unwrap();
    assert!(out.lines().filter(|s| !s.is_empty()).is_sorted());
}
