// Copyright 2021 Chad Williamson <chad@dahc.us>
//
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file or at https://opensource.org/licenses/MIT.

// Integration tests for the find command using the uutests framework.

use regex::Regex;
use std::fs::{self, File};
use std::io::ErrorKind;
use std::io::{Read, Write};
use std::path::Path;
use std::time;
use tempfile::Builder;
use uutests::util::TestScenario;

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

#[cfg(windows)]
use std::os::windows::fs::{symlink_dir, symlink_file};

use common::test_helpers::fix_up_slashes;

mod common;

/// Returns a UCommand for `find` with the working directory set to the
/// repository root, so that tests using relative `test_data/` paths work.
fn ucmd() -> uutests::util::UCommand {
    let ts = TestScenario::new("find");
    let mut cmd = ts.cmd(env!("CARGO_BIN_EXE_find"));
    cmd.current_dir(env!("CARGO_MANIFEST_DIR"));
    cmd
}

// Variants of fix_up_slashes that properly escape the forward slashes for
// use in a regex.
#[cfg(windows)]
fn fix_up_regex_slashes(re: &str) -> String {
    re.replace("/", "\\\\")
}

#[cfg(not(windows))]
fn fix_up_regex_slashes(re: &str) -> String {
    re.to_owned()
}

#[test]
fn no_args() {
    ucmd().succeeds().no_stderr().stdout_contains("test_data");
}

#[test]
fn two_matchers_both_match() {
    ucmd()
        .args(&["-type", "d", "-name", "test_data"])
        .succeeds()
        .no_stderr()
        .stdout_contains("test_data");
}

#[test]
fn two_matchers_one_matches() {
    ucmd()
        .args(&["-type", "f", "-name", "test_data"])
        .succeeds()
        .no_output();
}

#[test]
fn multiple_matcher_success() {
    ucmd()
        .args(&["-type", "f,d,l", "-name", "abbbc"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");

    ucmd()
        .args(&["-xtype", "f,d,l", "-name", "abbbc"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");
}

#[test]
fn invalid_newerxy_predicate_is_rejected() {
    // GNU find rejects over-long / invalid -newerXY tokens instead of matching a
    // valid prefix (issue #782: -neweraBcmty was accepted as -neweraB).
    ucmd()
        .args(&["-neweraBcmty", "."])
        .fails()
        .stderr_contains("unknown predicate `-neweraBcmty'")
        .no_stdout();

    ucmd()
        .args(&["-newermmEXTRA", "."])
        .fails()
        .stderr_contains("unknown predicate `-newermmEXTRA'")
        .no_stdout();
}

#[test]
fn multiple_matcher_failure() {
    ucmd()
        .args(&["-type", "fd", "-name", "abbb"])
        .fails()
        .stderr_contains("Must separate multiple arguments")
        .no_stdout();

    ucmd()
        .args(&["-type", "f,", "-name", "abbb"])
        .fails()
        .stderr_contains("list is ending on: ','")
        .no_stdout();

    ucmd()
        .args(&["-type", "f,f", "-name", "abbb"])
        .fails()
        .stderr_contains("Duplicate file type")
        .no_stdout();

    ucmd()
        .args(&["-type", "", "-name", "abbb"])
        .fails()
        .stderr_contains("should contain at least one letter")
        .no_stdout();

    ucmd()
        .args(&["-type", "x,y", "-name", "abbb"])
        .fails()
        .stderr_contains("Unrecognised type argument")
        .no_stdout();

    // x-type tests below
    ucmd()
        .args(&["-xtype", "fd", "-name", "abbb"])
        .fails()
        .stderr_contains("Must separate multiple arguments")
        .no_stdout();

    ucmd()
        .args(&["-xtype", "f,", "-name", "abbb"])
        .fails()
        .stderr_contains("list is ending on: ','")
        .no_stdout();

    ucmd()
        .args(&["-xtype", "f,f", "-name", "abbb"])
        .fails()
        .stderr_contains("Duplicate file type")
        .no_stdout();

    ucmd()
        .args(&["-xtype", "", "-name", "abbb"])
        .fails()
        .stderr_contains("should contain at least one letter")
        .no_stdout();

    ucmd()
        .args(&["-xtype", "x,y", "-name", "abbb"])
        .fails()
        .stderr_contains("Unrecognised type argument")
        .no_stdout();
}

#[test]
fn files0_empty_file() {
    ucmd()
        .args(&["-files0-from", "./test_data/simple/abbbc"])
        .succeeds()
        .no_output();
}

#[test]
fn files0_file_basic_success() {
    ucmd()
        .args(&["-files0-from", "./test_data/simple/abbbc"])
        .succeeds()
        .no_output();

    #[cfg(unix)]
    {
        let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap();
        let test_file = temp_dir.path().join("test_files0");
        let mut file = File::create(&test_file).expect("created test file");
        file.write_all(b"./test_data/\0./test_data/simple/\0")
            .expect("file write error");

        ucmd()
            .args(&["-files0-from", &test_file.display().to_string()])
            .succeeds()
            .no_stderr()
            .stdout_contains("/test_data/");
    }
}

#[test]
fn files0_invalid_utf8_fails() {
    let temp_dir = Builder::new().prefix("find_files0_").tempdir().unwrap();
    let path_list = temp_dir.path().join("paths0");
    fs::write(&path_list, b"\xff\nfile2.txt\n").expect("wrote path list");

    ucmd()
        .arg("-files0-from")
        .arg(path_list)
        .fails()
        .stderr_contains("invalid utf-8 sequence")
        .no_stdout();
}

#[test]
fn files0_empty_pipe() {
    ucmd()
        .args(&["-files0-from", "-"])
        .pipe_in(b"" as &[u8])
        .succeeds()
        .no_output();
}

#[test]
fn files0_pipe_basic() {
    ucmd()
        .pipe_in(b"./test_data/simple\0./test_data/links" as &[u8])
        .args(&["-files0-from", "-"])
        .succeeds()
        .no_stderr()
        .stdout_contains("./test_data/");
}

#[test]
fn files0_pipe_double_nul() {
    ucmd()
        .pipe_in(b"./test_data/simple\0\0./test_data/links" as &[u8])
        .args(&["-files0-from", "-"])
        .succeeds()
        .stderr_contains("invalid zero-length file name")
        .stdout_contains("./test_data/");
}

#[test]
fn files0_no_file() {
    #[cfg(unix)]
    {
        ucmd()
            .args(&["-files0-from", "xyz.nonexistentFile"])
            .fails()
            .stderr_contains("No such file or directory")
            .no_stdout();
    }
    #[cfg(windows)]
    {
        ucmd()
            .args(&["-files0-from", "xyz.nonexistantFile"])
            .fails()
            .stderr_contains("The system cannot find the file specified.")
            .no_stdout();
    }
}

#[test]
fn files0_basic() {
    ucmd()
        .arg("-files0-from")
        .fails()
        .stderr_contains("missing argument to -files0-from")
        .no_stdout();
}

#[test]
fn matcher_with_side_effects_at_end() {
    let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap();

    let temp_dir_path = temp_dir.path().to_str().unwrap();
    let test_file = temp_dir.path().join("test");
    File::create(&test_file).expect("created test file");

    ucmd()
        .args(&[temp_dir_path, "-name", "test", "-delete"])
        .succeeds()
        .no_output();

    assert!(!test_file.exists(), "test file should be deleted");
    assert!(temp_dir.path().exists(), "temp dir should NOT be deleted");
}

#[test]
fn matcher_with_side_effects_in_front() {
    let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap();

    let temp_dir_path = temp_dir.path().to_str().unwrap();
    let test_file = temp_dir.path().join("test");
    File::create(&test_file).expect("created test file");

    ucmd()
        .args(&[temp_dir_path, "-delete", "-name", "test"])
        .succeeds()
        .no_output();

    assert!(!test_file.exists(), "test file should be deleted");
    assert!(!temp_dir.path().exists(), "temp dir should also be deleted");
}

// This could be covered by a unit test in principle... in practice, changing
// the working dir can't be done safely in unit tests unless `--test-threads=1`
// or `serial` goes everywhere, and it doesn't seem possible to get an
// appropriate `walkdir::DirEntry` for "." without actually changing dirs
// (or risking deletion of the repo itself).
#[test]
fn delete_on_dot_dir() {
    let temp_dir = Builder::new().prefix("example").tempdir().unwrap();

    // "." should be matched (confirmed by the print), but not deleted.
    ucmd()
        .current_dir(temp_dir.path())
        .args(&[".", "-delete", "-print"])
        .succeeds()
        .no_stderr()
        .stdout_only(".\n");

    assert!(temp_dir.path().exists(), "temp dir should still exist");
}

#[test]
fn regex_types() {
    let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap();

    let temp_dir_path = temp_dir.path().to_str().unwrap();
    let test_file = temp_dir.path().join("teeest");
    File::create(test_file).expect("created test file");

    ucmd()
        .arg(temp_dir_path)
        .arg("-regex")
        .arg(fix_up_regex_slashes(".*/tE+st"))
        .succeeds()
        .no_output();

    ucmd()
        .arg(temp_dir_path)
        .arg("-iregex")
        .arg(fix_up_regex_slashes(".*/tE+st"))
        .succeeds()
        .no_stderr()
        .stdout_contains("teeest");

    ucmd()
        .arg(temp_dir_path)
        .args(&["-regextype", "posix-basic", "-regex"])
        .arg(fix_up_regex_slashes(r".*/te\{1,3\}st"))
        .succeeds()
        .no_stderr()
        .stdout_contains("teeest");

    ucmd()
        .arg(temp_dir_path)
        .args(&["-regextype", "posix-extended", "-regex"])
        .arg(fix_up_regex_slashes(".*/te{1,3}st"))
        .succeeds()
        .no_stderr()
        .stdout_contains("teeest");

    ucmd()
        .arg(temp_dir_path)
        .args(&["-regextype", "ed", "-regex"])
        .arg(fix_up_regex_slashes(r".*/te\{1,3\}st"))
        .succeeds()
        .no_stderr()
        .stdout_contains("teeest");

    ucmd()
        .arg(temp_dir_path)
        .args(&["-regextype", "sed", "-regex"])
        .arg(fix_up_regex_slashes(r".*/te\{1,3\}st"))
        .succeeds()
        .no_stderr()
        .stdout_contains("teeest");
}

#[test]
fn empty_files() {
    let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap();
    let temp_dir_path = temp_dir.path().to_str().unwrap();

    ucmd()
        .args(&[temp_dir_path, "-empty"])
        .succeeds()
        .no_stderr()
        .stdout_only(fix_up_slashes(&format!("{temp_dir_path}\n")));

    let test_file_path = temp_dir.path().join("test");
    let mut test_file = File::create(&test_file_path).unwrap();

    ucmd()
        .args(&[temp_dir_path, "-empty"])
        .succeeds()
        .no_stderr()
        .stdout_only(fix_up_slashes(&format!(
            "{}\n",
            test_file_path.to_string_lossy()
        )));

    let subdir_path = temp_dir.path().join("subdir");
    std::fs::create_dir(&subdir_path).unwrap();

    ucmd()
        .args(&[temp_dir_path, "-empty", "-sorted"])
        .succeeds()
        .no_stderr()
        .stdout_only(fix_up_slashes(&format!(
            "{}\n{}\n",
            subdir_path.to_string_lossy(),
            test_file_path.to_string_lossy()
        )));

    write!(test_file, "x").unwrap();
    test_file.sync_all().unwrap();

    ucmd()
        .args(&[temp_dir_path, "-empty", "-sorted"])
        .succeeds()
        .no_stderr()
        .stdout_only(fix_up_slashes(&format!(
            "{}\n",
            subdir_path.to_string_lossy(),
        )));
}

#[test]
fn find_printf() {
    #[cfg(unix)]
    {
        if let Err(e) = symlink("abbbc", "test_data/links/link-f") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {e:?}"
            );
        }
        if let Err(e) = symlink("subdir", "test_data/links/link-d") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {e:?}"
            );
        }
        if let Err(e) = symlink("missing", "test_data/links/link-missing") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {e:?}"
            );
        }
        if let Err(e) = symlink("abbbc/x", "test_data/links/link-notdir") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {e:?}"
            );
        }
        if let Err(e) = symlink("link-loop", "test_data/links/link-loop") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {e:?}"
            );
        }
    }
    #[cfg(windows)]
    {
        if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {:?}",
                e
            );
        }
        if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {:?}",
                e
            );
        }
        if let Err(e) = symlink_file("missing", "test_data/links/link-missing") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {:?}",
                e
            );
        }
        if let Err(e) = symlink_file("abbbc/x", "test_data/links/link-notdir") {
            assert!(
                e.kind() == ErrorKind::AlreadyExists,
                "Failed to create sym link: {:?}",
                e
            );
        }
    }

    ucmd()
        .arg(fix_up_slashes("./test_data/simple"))
        .args(&["-sorted", "-printf", "%f %d %h %H %p %P %y\n"])
        .succeeds()
        .no_stderr()
        .stdout_only(fix_up_slashes(
            "simple 0 ./test_data ./test_data/simple \
            ./test_data/simple  d\n\
            abbbc 1 ./test_data/simple ./test_data/simple \
            ./test_data/simple/abbbc abbbc f\n\
            subdir 1 ./test_data/simple ./test_data/simple \
            ./test_data/simple/subdir subdir d\n\
            ABBBC 2 ./test_data/simple/subdir ./test_data/simple \
            ./test_data/simple/subdir/ABBBC subdir/ABBBC f\n",
        ));

    fs::create_dir_all("a").expect("Failed to create directory 'a'");
    let result = ucmd().args(&["a", "-printf", "%A+"]).succeeds();

    let output_str = result.stdout_str();
    let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\+\d{2}:\d{2}:\d{2}\.\d{9}0$")
        .expect("Failed to compile regex");
    assert!(
        re.is_match(output_str.trim()),
        "Output did not match expected timestamp format"
    );

    ucmd()
        .arg(fix_up_slashes("./test_data/links"))
        .args(&["-sorted", "-type", "l", "-printf", "%f %l %y %Y\n"])
        .succeeds()
        .no_stderr()
        .stdout_only(
            [
                "link-d subdir l d\n",
                "link-f abbbc l f\n",
                #[cfg(unix)]
                "link-loop link-loop l L\n",
                "link-missing missing l N\n",
                // We can't detect ENOTDIR on non-unix platforms yet.
                #[cfg(not(unix))]
                "link-notdir abbbc/x l ?\n",
                #[cfg(unix)]
                "link-notdir abbbc/x l N\n",
            ]
            .join(""),
        );
}

#[test]
#[cfg(unix)] // %T+ prints local time; TZ only reliably selects it on unix
fn find_printf_time_plus_fixed_width_fraction() {
    let temp_dir = Builder::new().prefix("find_time_plus_").tempdir().unwrap();
    let file_path = temp_dir.path().join("stamped");
    File::create(&file_path).expect("create test file");

    // The fraction must always be a dot followed by ten digits, even when
    // the timestamp falls on a whole second.
    for (nanos, expected) in [
        (0, "2013-06-20+11:22:33.0000000000\n"),
        (250_000_000, "2013-06-20+11:22:33.2500000000\n"),
    ] {
        filetime::set_file_mtime(
            &file_path,
            filetime::FileTime::from_unix_time(1_371_727_353, nanos),
        )
        .expect("set test file mtime");

        ucmd()
            .env("TZ", "UTC0")
            .arg(&file_path)
            .args(&["-printf", "%T+\n"])
            .succeeds()
            .stdout_only(expected);
    }
}

#[test]
fn find_printf_octal_escape_before_multibyte_char() {
    ucmd()
        .args(&["./test_data/simple", "-maxdepth", "0", "-printf", "\\0€\\n"])
        .succeeds()
        .no_stderr()
        .stdout_only("\0€\n");
}

#[test]
fn find_printf_width_too_large() {
    ucmd()
        .args(&[
            "./test_data/simple",
            "-maxdepth",
            "0",
            "-printf",
            "%70000s\\n",
        ])
        .fails()
        .stderr_contains("find: Format width too large");
    ucmd()
        .args(&[
            "./test_data/simple",
            "-maxdepth",
            "0",
            "-printf",
            "%99999999999999999999s\\n",
        ])
        .fails()
        .stderr_contains("find: Invalid format width");
}

#[cfg(unix)]
#[test]
fn find_perm() {
    ucmd().args(&["-perm", "+rwx"]).succeeds();
    ucmd().args(&["-perm", "u+rwX"]).succeeds();
    ucmd().args(&["-perm", "u=g"]).succeeds();
}

#[cfg(unix)]
#[test]
fn find_inum() {
    use std::os::unix::fs::MetadataExt;

    let inum = fs::metadata("test_data/simple/abbbc")
        .expect("metadata for abbbc")
        .ino()
        .to_string();

    ucmd()
        .args(&["test_data", "-inum", &inum])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");
}

#[cfg(not(unix))]
#[test]
fn find_inum() {
    ucmd()
        .args(&["test_data", "-inum", "1"])
        .fails()
        .stderr_contains("not available on this platform")
        .no_stdout();
}

#[cfg(unix)]
#[test]
fn find_links() {
    ucmd()
        .args(&["test_data", "-links", "1"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");
}

#[cfg(not(unix))]
#[test]
fn find_links() {
    ucmd()
        .args(&["test_data", "-links", "1"])
        .fails()
        .stderr_contains("not available on this platform")
        .no_stdout();
}

#[test]
fn find_mount_xdev() {
    // Make sure that -mount/-xdev doesn't prune unexpectedly.
    // TODO: Test with a mount point in the search.
    ucmd()
        .args(&["test_data", "-mount"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");

    ucmd()
        .args(&["test_data", "-xdev"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");
}

#[test]
fn find_accessible() {
    ucmd()
        .args(&["test_data", "-readable"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");

    ucmd()
        .args(&["test_data", "-writable"])
        .succeeds()
        .no_stderr()
        .stdout_contains("abbbc");

    #[cfg(unix)]
    ucmd()
        .args(&["test_data", "-executable"])
        .succeeds()
        .no_stderr()
        .stdout_does_not_contain("abbbc");
}

#[test]
fn find_time() {
    let args = ["1", "+1", "-1"];
    let exception_args = ["1%2", "1%2%3", "1a2", "1%2a", "abc", "-", "+", "%"];

    let tests = [
        "-atime",
        #[cfg(unix)]
        "-ctime",
        "-mtime",
    ];
    for flag in &tests {
        for arg in &args {
            ucmd()
                .args(&["./test_data/simple", flag, arg])
                .succeeds()
                .no_stderr();
        }

        for arg in &exception_args {
            ucmd().args(&[".", flag, arg]).fails().no_stdout();
        }
    }
}

#[test]
fn expression_empty_parentheses() {
    ucmd()
        .args(&["-true", "(", ")"])
        .fails()
        .stderr_contains("empty parentheses are not allowed")
        .no_stdout();
}

#[test]
#[cfg(unix)]
fn find_with_user_predicate() {
    // Considering the different test environments,
    // the test code can only use a specific default user to perform the test,
    // such as the root user on Linux.
    ucmd()
        .args(&["test_data", "-user", "root"])
        .succeeds()
        .no_stderr();

    ucmd()
        .args(&["test_data", "-user", ""])
        .fails()
        .stderr_contains("empty")
        .no_stdout();

    ucmd()
        .args(&["test_data", "-user", " "])
        .fails()
        .stderr_contains("invalid user name or UID argument to -user")
        .no_stdout();
}

#[test]
fn find_with_nouser_predicate() {
    ucmd()
        .args(&["test_data", "-nouser"])
        .succeeds()
        .no_stdout()
        .no_stderr();
}

#[test]
#[cfg(unix)]
fn find_with_uid_predicate() {
    use std::os::unix::fs::MetadataExt;

    let uid = Path::new("./test_data")
        .metadata()
        .unwrap()
        .uid()
        .to_string();

    ucmd()
        .args(&["test_data", "-uid", &uid])
        .succeeds()
        .no_stderr();
}

#[test]
fn find_with_group_predicate() {
    // Considering the different test environments,
    // the test code can only use a specific default user group for the test,
    // such as the root user group on Linux.
    #[cfg(target_os = "linux")]
    ucmd()
        .args(&["test_data", "-group", "root"])
        .succeeds()
        .no_stderr();

    #[cfg(target_os = "macos")]
    ucmd()
        .args(&["test_data", "-group", "staff"])
        .succeeds()
        .no_stderr();

    ucmd()
        .args(&["test_data", "-group", ""])
        .fails()
        .stderr_contains("empty")
        .no_stdout();

    ucmd()
        .args(&["test_data", "-group", " "])
        .fails()
        .stderr_contains("invalid group name or GID argument to -group:")
        .no_stdout();
}

#[test]
fn find_with_nogroup_predicate() {
    ucmd()
        .args(&["test_data", "-nogroup"])
        .succeeds()
        .no_stdout()
        .no_stderr();
}

#[test]
#[cfg(unix)]
fn find_with_gid_predicate() {
    use std::os::unix::fs::MetadataExt;

    let gid = Path::new("./test_data")
        .metadata()
        .unwrap()
        .gid()
        .to_string();

    ucmd()
        .args(&["test_data", "-gid", &gid])
        .succeeds()
        .no_stderr();
}

#[test]
fn find_newer_xy() {
    let options = [
        "a",
        #[cfg(not(target_os = "linux"))]
        "B",
        #[cfg(unix)]
        "c",
        "m",
    ];

    for x in options {
        for y in options {
            let arg = &format!("-newer{x}{y}");
            ucmd()
                .args(&[
                    "./test_data/simple/subdir",
                    arg,
                    "./test_data/simple/subdir/ABBBC",
                ])
                .succeeds()
                .no_stderr();
        }
    }

    let args = [
        "-newerat",
        #[cfg(not(target_os = "linux"))]
        "-newerBt",
        #[cfg(unix)]
        "-newerct",
        "-newermt",
    ];
    let times = ["jan 01, 2000", "jan 01, 2000 00:00:00"];

    for arg in args {
        for time in times {
            ucmd()
                .args(&["./test_data/simple/subdir", arg, time])
                .succeeds()
                .no_stderr();
        }

        ucmd().args(&[".", arg, "invalid"]).fails().stderr_only(
            "find: I cannot figure out how to interpret ‘invalid’ as a date or time\n",
        );
    }

    #[cfg(target_os = "linux")]
    ucmd()
        .args(&[".", "-newerBt", "jan 01, 2000"])
        .fails()
        .stderr_only(
            "find: This system does not provide a way to find the birth time of a file.\n",
        );
}

#[test]
fn find_age_range() {
    let args = ["-amin", "-cmin", "-mmin"];
    let times = ["-60", "-120", "-240", "+60", "+120", "+240"];
    let time_strings = [
        "\"-60\"", "\"-120\"", "\"-240\"", "\"-60\"", "\"-120\"", "\"-240\"",
    ];

    for arg in args {
        for time in times {
            ucmd().args(&["test_data/simple", arg, time]).succeeds();
        }
    }

    for arg in args {
        for time_string in time_strings {
            ucmd()
                .args(&["test_data/simple", arg, time_string])
                .fails()
                .stderr_contains(
                    "find: Expected a decimal integer (with optional + or - prefix) argument to",
                )
                .no_stdout();
        }
    }
}

#[test]
#[cfg(unix)]
fn find_fs() {
    use findutils::find::matchers::fs::get_file_system_type;
    use std::cell::RefCell;

    let path = Path::new("./test_data/simple/subdir");
    let empty_cache = RefCell::new(None);
    let target_fs_type = get_file_system_type(path, &empty_cache).unwrap();

    // match fs type
    ucmd()
        .args(&["./test_data/simple/subdir", "-fstype", &target_fs_type])
        .succeeds()
        .stdout_contains("./test_data/simple/subdir")
        .no_stderr();

    // not match fs type
    ucmd()
        .args(&[
            "./test_data/simple/subdir",
            "-fstype",
            format!("{} foo", target_fs_type).as_str(),
        ])
        .succeeds()
        .no_output();

    // not contain fstype text.
    ucmd()
        .args(&["./test_data/simple/subdir", "-fstype"])
        .fails()
        .no_stdout();

    // void fstype
    ucmd()
        .args(&["./test_data/simple/subdir", "-fstype", " "])
        .succeeds()
        .no_output();

    let path = Path::new("./test_data/links");
    let empty_cache = RefCell::new(None);
    let target_fs_type = get_file_system_type(path, &empty_cache).unwrap();

    // working with broken links
    ucmd()
        .args(&["./test_data/links", "-fstype", &target_fs_type])
        .succeeds()
        .stdout_contains("./test_data/links/link-missing")
        .no_stderr();
}

#[test]
fn find_samefile() {
    let temp_dir = Builder::new().prefix("find_samefile_").tempdir().unwrap();
    let temp_dir_path = temp_dir.path().to_str().unwrap();
    let test_file = temp_dir.path().join("abbbc");
    let hard_link = temp_dir.path().join("hard_link");
    fs::copy("test_data/links/abbbc", &test_file).unwrap();
    fs::hard_link(&test_file, &hard_link).unwrap();

    let test_file_str = test_file.to_str().unwrap();
    let hard_link_str = hard_link.to_str().unwrap();
    let not_exist = temp_dir.path().join("not-exist-file");
    let not_exist_str = not_exist.to_str().unwrap();

    ucmd()
        .args(&[test_file_str, "-samefile", hard_link_str])
        .succeeds()
        .stdout_contains(test_file_str)
        .no_stderr();

    // test . path
    ucmd()
        .args(&[temp_dir_path, "-samefile", temp_dir_path])
        .succeeds()
        .stdout_contains(temp_dir_path)
        .no_stderr();

    ucmd()
        .args(&[temp_dir_path, "-samefile", test_file_str])
        .succeeds()
        .stdout_contains(fix_up_slashes(test_file_str))
        .no_stderr();

    // test not exist file
    ucmd()
        .args(&[temp_dir_path, "-samefile", not_exist_str])
        .fails()
        .no_stdout()
        .stderr_contains("not-exist-file");
}

#[test]
fn find_noleaf() {
    ucmd()
        .args(&["test_data/simple/subdir", "-noleaf"])
        .succeeds()
        .stdout_contains("test_data/simple/subdir")
        .no_stderr();
}

#[test]
fn find_daystart() {
    ucmd()
        .args(&["./test_data/simple/subdir", "-daystart", "-mtime", "0"])
        .succeeds()
        .no_stderr();

    // twice -daystart should be matched
    ucmd()
        .args(&[
            "./test_data/simple/subdir",
            "-daystart",
            "-daystart",
            "-mtime",
            "1",
        ])
        .succeeds()
        .no_stderr();
}

#[test]
fn find_fprinter() {
    let temp_dir = Builder::new().prefix("find_fprinter_").tempdir().unwrap();
    let printer = ["fprint", "fprint0"];

    for p in &printer {
        let out_file = temp_dir.path().join(format!("find_{p}"));
        let out_file_str = out_file.to_str().unwrap();

        ucmd()
            .args(&["test_data/simple", format!("-{p}").as_str(), out_file_str])
            .succeeds()
            .no_output();

        // read test_data/find_fprint
        let mut f = File::open(&out_file).unwrap();
        let mut contents = String::new();
        f.read_to_string(&mut contents).unwrap();
        assert!(contents.contains("test_data/simple"));
    }
}

#[test]
fn find_follow() {
    ucmd()
        .args(&["test_data/links/link-f", "-follow"])
        .succeeds()
        .stdout_contains("test_data/links/link-f")
        .no_stderr();
}

#[test]
fn find_fprintf() {
    let temp_dir = Builder::new().prefix("find_fprintf_").tempdir().unwrap();
    let out_file = temp_dir.path().join("find_fprintf");
    let out_file_str = out_file.to_str().unwrap();

    ucmd()
        .args(&["test_data/simple", "-fprintf", out_file_str, "%h %H %p %P"])
        .succeeds()
        .no_output();

    // read test_data/find_fprintf
    let mut f = File::open(&out_file).unwrap();
    let mut contents = String::new();
    f.read_to_string(&mut contents).unwrap();
    assert!(contents.contains("test_data/simple"));
}

#[test]
fn find_fprintf_missing_arguments() {
    // Regression test: `-fprintf` with no file/format argument must report a
    // missing-argument error instead of panicking (see issue #696).
    ucmd()
        .args(&["-fprintf"])
        .fails()
        .stderr_contains("missing argument to -fprintf");

    ucmd()
        .args(&["-fprintf", "/tmp/find_fprintf_out"])
        .fails()
        .stderr_contains("missing argument to -fprintf");
}

#[test]
fn find_ls() {
    ucmd()
        .args(&["./test_data/simple/subdir", "-ls"])
        .succeeds()
        .no_stderr();
}

// Regression test for uutils/findutils#717: `-ls` used to abort (exit 101) when
// a file's owning uid/gid had no passwd/group entry; it must fall back to the
// numeric id like GNU find. Creating such a file needs privilege to chown to an
// unmapped id, so skip when that isn't available (e.g. non-root CI).
#[test]
#[cfg(unix)]
fn find_ls_unmapped_owner_renders_numeric_id() {
    use nix::unistd::{chown, Gid, Uid};

    let temp_dir = Builder::new().prefix("find_ls_unmapped").tempdir().unwrap();
    let file = temp_dir.path().join("orphan");
    File::create(&file).unwrap();

    // A uid/gid essentially never present in /etc/passwd or /etc/group.
    let unmapped = 4_000_000_000u32;
    if chown(
        &file,
        Some(Uid::from_raw(unmapped)),
        Some(Gid::from_raw(unmapped)),
    )
    .is_err()
    {
        return;
    }

    ucmd()
        .args(&[file.to_str().unwrap(), "-ls"])
        .succeeds()
        .stdout_contains(unmapped.to_string());
}

#[test]
#[cfg(unix)]
fn find_slashes() {
    ucmd()
        .args(&["///", "-maxdepth", "0", "-name", "/"])
        .succeeds()
        .no_stderr();
}

// -ok / -okdir integration tests
//
// These tests use pipe_in() to supply the user's response.  Because pipe_in()
// makes stdin a pipe, std::io::stdin().is_terminal() returns false inside the
// find subprocess, so StandardDependencies::confirm reads from stdin directly
// instead of opening /dev/tty.  No special environment variable is needed.

#[test]
fn find_ok_yes_runs_command() {
    // When the user answers "y", -ok should run the command and print output.
    ucmd()
        .args(&[
            "test_data/simple",
            "-maxdepth",
            "1",
            "-name",
            "abbbc",
            "-ok",
            "echo",
            "{}",
            ";",
        ])
        // Pipe the affirmative response for the single file found.
        .pipe_in("y\n")
        .succeeds()
        .stderr_contains("< echo") // prompt appeared
        .stdout_contains("abbbc"); // echo ran
}

#[test]
fn find_ok_no_skips_command() {
    // When the user answers "n", -ok should not run the command.
    // The expression is false so no output is produced, but find exits 0.
    ucmd()
        .args(&[
            "test_data/simple",
            "-maxdepth",
            "1",
            "-name",
            "abbbc",
            "-ok",
            "echo",
            "{}",
            ";",
        ])
        .pipe_in("n\n")
        .succeeds()
        .stderr_contains("< echo") // prompt still appeared
        .no_stdout(); // but echo was not run
}

#[test]
fn find_ok_prompt_format() {
    // The prompt should follow GNU find's "< executable args... >? " format.
    ucmd()
        .args(&[
            "test_data/simple",
            "-maxdepth",
            "1",
            "-name",
            "abbbc",
            "-ok",
            "echo",
            "{}",
            ";",
        ])
        .pipe_in("n\n")
        .succeeds()
        .stderr_contains(format!(
            "< echo ... {} > ? ",
            Path::new("test_data/simple")
                .join("abbbc")
                .to_string_lossy()
        ));
}

#[test]
fn find_ok_empty_response_declines() {
    // An empty line (just Enter) should be treated as decline.
    ucmd()
        .args(&[
            "test_data/simple",
            "-maxdepth",
            "1",
            "-name",
            "abbbc",
            "-ok",
            "echo",
            "{}",
            ";",
        ])
        .pipe_in("\n")
        .succeeds()
        .no_stdout();
}

#[test]
fn find_ok_accepts_y_variants() {
    // "Y", "yes", and " y" (leading whitespace) should all be accepted.
    for response in &["Y\n", "yes\n", " y\n"] {
        ucmd()
            .args(&[
                "test_data/simple",
                "-maxdepth",
                "1",
                "-name",
                "abbbc",
                "-ok",
                "echo",
                "{}",
                ";",
            ])
            .pipe_in(*response)
            .succeeds()
            .stdout_contains("abbbc");
    }
}

#[test]
fn find_okdir_yes_runs_command() {
    // -okdir should run the command in the file's parent directory.
    ucmd()
        .args(&[
            "test_data/simple",
            "-maxdepth",
            "1",
            "-name",
            "abbbc",
            "-okdir",
            "echo",
            "{}",
            ";",
        ])
        .pipe_in("y\n")
        .succeeds()
        .stderr_contains("< echo")
        .stdout_contains(fix_up_slashes("./abbbc"));
}

#[test]
fn find_ok_missing_semicolon() {
    // -ok without a closing ';' should be an error (just like -exec).
    ucmd()
        .args(&["test_data/simple", "-ok", "echo", "{}"])
        .pipe_in("")
        .fails()
        .stderr_contains("missing argument to -ok")
        .no_stdout();
}

#[test]
fn version_write_error_is_handled() {
    use std::cell::RefCell;

    struct BrokenWriter;
    impl std::io::Write for BrokenWriter {
        fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
            Err(std::io::Error::from_raw_os_error(28))
        }
        fn flush(&mut self) -> std::io::Result<()> {
            Err(std::io::Error::from_raw_os_error(28))
        }
    }

    struct BrokenDependencies {
        output: RefCell<BrokenWriter>,
    }

    impl BrokenDependencies {
        fn new() -> Self {
            Self {
                output: RefCell::new(BrokenWriter),
            }
        }
    }

    impl findutils::find::Dependencies for BrokenDependencies {
        fn get_output(&self) -> &RefCell<dyn Write> {
            &self.output
        }

        fn now(&self) -> time::SystemTime {
            time::SystemTime::now()
        }

        fn confirm(&self, _prompt: &str) -> bool {
            false
        }
    }

    let deps = BrokenDependencies::new();
    let rc = findutils::find::find_main(&["find", "--version"], &deps);
    assert_eq!(rc, 1);
}

#[test]
fn print_write_error_is_reported_not_swallowed() {
    // Regression test: a write/flush failure other than a broken pipe
    // (e.g. a full disk) must be reported with a nonzero exit code,
    // not silently ignored or turned into a panic.
    use std::cell::RefCell;

    struct BrokenWriter;
    impl std::io::Write for BrokenWriter {
        fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
            Err(std::io::Error::from_raw_os_error(28)) // ENOSPC
        }
        fn flush(&mut self) -> std::io::Result<()> {
            Err(std::io::Error::from_raw_os_error(28))
        }
    }

    struct BrokenDependencies {
        output: RefCell<BrokenWriter>,
    }

    impl BrokenDependencies {
        fn new() -> Self {
            Self {
                output: RefCell::new(BrokenWriter),
            }
        }
    }

    impl findutils::find::Dependencies for BrokenDependencies {
        fn get_output(&self) -> &RefCell<dyn Write> {
            &self.output
        }

        fn now(&self) -> time::SystemTime {
            time::SystemTime::now()
        }

        fn confirm(&self, _prompt: &str) -> bool {
            false
        }
    }

    let deps = BrokenDependencies::new();
    let rc = findutils::find::find_main(&["find", "test_data/simple", "-maxdepth", "0"], &deps);
    assert_eq!(rc, 1);
}

#[test]
fn find_exits_cleanly_on_broken_pipe() {
    // Regression test: `find | head` must exit cleanly instead of
    // panicking when the reader closes its end of the pipe early. (See issue#801)
    use std::io::Read;
    use std::process::{Command, Stdio};

    let mut child = Command::new(env!("CARGO_BIN_EXE_find"))
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .arg(".")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    // Drop the read end without reading, so `find`'s writes fail with
    // EPIPE once it produces more output than the pipe buffer holds.
    drop(child.stdout.take());

    let status = child.wait().unwrap();
    let mut stderr = String::new();
    child
        .stderr
        .take()
        .unwrap()
        .read_to_string(&mut stderr)
        .unwrap();

    assert!(
        status.success(),
        "find should exit cleanly on a broken pipe instead of panicking, \
         got status {status:?}, stderr: {stderr}"
    );
    assert!(
        !stderr.contains("panicked"),
        "find panicked instead of exiting cleanly on a broken pipe:\n{stderr}"
    );
}
