// 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 (words) araba newroot userspec chdir pwd's isroot

use uutests::at_and_ucmd;
use uutests::new_ucmd;
#[cfg(not(target_os = "android"))]
use uutests::util::is_ci;
use uutests::util::{TestScenario, run_ucmd_as_root};
use uutests::util_name;

#[test]
fn test_invalid_arg() {
    new_ucmd!().arg("--definitely-invalid").fails_with_code(125);
}

#[test]
fn test_missing_operand() {
    let result = new_ucmd!().fails_with_code(125);

    assert!(
        result
            .stderr_str()
            .starts_with("error: the following required arguments were not provided")
    );

    assert!(result.stderr_str().contains("<newroot>"));
}

#[test]
#[cfg(not(target_os = "android"))]
fn test_enter_chroot_fails() {
    // NOTE: since #2689 this test also ensures that we don't regress #2687
    let (at, mut ucmd) = at_and_ucmd!();

    at.mkdir("jail");

    let result = ucmd.arg("jail").fails_with_code(125);
    assert!(
        result
            .stderr_str()
            .starts_with("chroot: cannot chroot to 'jail': Operation not permitted (os error 1)")
    );
}

#[test]
fn test_no_such_directory() {
    let (at, mut ucmd) = at_and_ucmd!();

    at.touch(at.plus_as_string("a"));

    ucmd.arg("a")
        .fails_with_code(125)
        .stderr_is("chroot: cannot change root directory to 'a': no such directory\n");
}

#[test]
fn test_multiple_group_args() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;
    at.mkdir("id");

    if let Ok(result) = run_ucmd_as_root(
        &ts,
        &["--groups='invalid ignored'", "--groups=''", "/", "id", "-G"],
    ) {
        result.success().stdout_is("0");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_invalid_user_spec() {
    let ts = TestScenario::new(util_name!());

    if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=ARABA:", "/"]) {
        result
            .failure()
            .code_is(125)
            .stderr_is("chroot: invalid user");
    } else {
        print!("Test skipped; requires root user");
    }

    if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=ARABA:ARABA", "/"]) {
        result
            .failure()
            .code_is(125)
            .stderr_is("chroot: invalid user");
    } else {
        print!("Test skipped; requires root user");
    }

    if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=:ARABA", "/"]) {
        result
            .failure()
            .code_is(125)
            .stderr_is("chroot: invalid group");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_invalid_user() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);
    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "whoami"]) {
        result.success().no_stderr().stdout_is("root");
    } else {
        print!("Test skipped; requires root user");
    }

    // `--user` is an abbreviation of `--userspec`.
    if let Ok(result) = run_ucmd_as_root(&ts, &["--user=nobody:+65535", dir, "pwd"]) {
        result.failure().stderr_is("chroot: invalid user");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
#[cfg(not(target_os = "android"))]
fn test_preference_of_userspec() {
    let scene = TestScenario::new(util_name!());
    let result = scene.cmd("whoami").run();
    if is_ci() && result.stderr_str().contains("No such user/group") {
        // In the CI, some server are failing to return whoami.
        // As seems to be a configuration issue, ignoring it
        return;
    }
    println!("result.stdout = {}", result.stdout_str());
    println!("result.stderr = {}", result.stderr_str());
    let username = result.stdout_str().trim_end();

    let ts = TestScenario::new("id");
    let result = ts.cmd("id").arg("-g").arg("-n").run();
    println!("result.stdout = {}", result.stdout_str());
    println!("result.stderr = {}", result.stderr_str());

    if is_ci() && result.stderr_str().contains("cannot find name for user ID") {
        // In the CI, some server are failing to return id.
        // As seems to be a configuration issue, ignoring it
        return;
    }

    let group_name = result.stdout_str().trim_end();
    let (at, mut ucmd) = at_and_ucmd!();

    at.mkdir("a");

    // `--user` is an abbreviation of `--userspec`.
    let result = ucmd
        .arg("a")
        .arg("--user")
        .arg("fake")
        .arg("--groups")
        .arg("ABC,DEF")
        .arg(format!("--userspec={username}:{group_name}"))
        .fails_with_code(125);

    println!("result.stdout = {}", result.stdout_str());
    println!("result.stderr = {}", result.stderr_str());
}

#[test]
fn test_default_shell() {
    // NOTE: This test intends to trigger code which can only be reached with root permissions.
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);

    let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
    let expected = format!("chroot: failed to run command '{shell}': No such file or directory");

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir]) {
        result.stderr_contains(expected);
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot_command_not_found_error() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);

    let missing = "definitely_missing_command";

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, missing]) {
        result
            .failure()
            .code_is(127)
            .stderr_contains(format!("failed to run command '{missing}'"))
            .stderr_contains("No such file or directory");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot_command_permission_denied_error() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);

    let script_path = format!("{dir}/noexec.sh");
    at.write(&script_path, "#!/bin/sh\necho unreachable\n");
    #[cfg(not(windows))]
    {
        at.set_mode(&script_path, 0o644);
    }

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "/noexec.sh"]) {
        result
            .failure()
            .code_is(126)
            .stderr_contains("failed to run command '/noexec.sh'")
            .stderr_contains("Permission denied");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);
    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "whoami"]) {
        result.success().no_stderr().stdout_is("root");
    } else {
        print!("Test skipped; requires root user");
    }

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "pwd"]) {
        result.success().no_stderr().stdout_is("/");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot_retains_uid_gid() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "id", "-u"]) {
        result.success().no_stderr().stdout_is("0");
    } else {
        print!("Test skipped; requires root user");
    }

    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "id", "-g"]) {
        result.success().no_stderr().stdout_is("0");
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot_skip_chdir_not_root() {
    let (at, mut ucmd) = at_and_ucmd!();

    let dir = "foobar";
    at.mkdir(dir);

    ucmd.arg("--skip-chdir")
        .arg(dir)
        .fails_with_code(125)
        .stderr_contains("chroot: option --skip-chdir only permitted if NEWROOT is old '/'");
}

#[test]
fn test_chroot_skip_chdir() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;
    let dirs = ["/", "/.", "/..", "isroot"];
    at.symlink_file("/", "isroot");
    for dir in dirs {
        let env_cd = std::env::current_dir().unwrap();
        if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "--skip-chdir"]) {
            // Should return the same path
            assert_eq!(
                result.success().no_stderr().stdout_str(),
                env_cd.to_str().unwrap()
            );
        } else {
            print!("Test skipped; requires root user");
        }
    }
}

#[test]
fn test_chroot_extra_arg() {
    let ts = TestScenario::new(util_name!());
    let at = &ts.fixtures;

    let dir = "CHROOT_DIR";
    at.mkdir(dir);
    let env_cd = std::env::current_dir().unwrap();
    // Verify that -P is pwd's and not chroot
    if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "pwd", "-P"]) {
        assert_eq!(
            result.success().no_stderr().stdout_str(),
            env_cd.to_str().unwrap()
        );
    } else {
        print!("Test skipped; requires root user");
    }
}

#[test]
fn test_chroot_userspec_does_not_set_gid_with_uid() {
    use uucore::entries::{usr2gid, usr2uid};

    let ts = TestScenario::new(util_name!());
    if let Ok(uid) = usr2uid("sync") {
        if let Ok(gid) = usr2gid("sync") {
            if gid == uid {
                println!("Test skipped; requires sync user to have uid != gid");
                return;
            }
            // Ubuntu has a sync user whose gid is 65534 per default
            if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=sync", "/", "id", "-g"]) {
                result.success().no_stderr().stdout_is(format!("{gid}\n"));
            } else {
                println!("Test skipped; requires root user");
            }
        } else {
            println!("Test skipped; requires 'sync' user");
        }
    } else {
        println!("Test skipped; requires 'sync' user");
    }
}

#[test]
fn test_chroot_userspec_unknown_uid() {
    let ts = TestScenario::new(util_name!());
    if let Ok(result) =
        run_ucmd_as_root(&ts, &["--userspec=99999", "--groups=root", "/", "id", "-g"])
    {
        result
            .failure()
            .code_is(125)
            .stderr_is("chroot: no group specified for unknown uid: 99999\n");
    } else {
        println!("Test skipped; requires root user");
    }
}
