// Copyright 2021 Collabora, Ltd.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

// Integration tests for the xargs command using the uutests framework.
// These are integration tests (rather than unit tests) so that the
// testing-commandline binary is guaranteed to be built first.
use uutests::util::TestScenario;

use common::test_helpers::path_to_testing_commandline;

mod common;

fn ucmd() -> uutests::util::UCommand {
    TestScenario::new("xargs").cmd(env!("CARGO_BIN_EXE_xargs"))
}

#[test]
fn xargs_basics() {
    ucmd()
        .pipe_in("abc\ndef g\\hi  'i  j \"k'")
        .succeeds()
        .stdout_only("abc def ghi i  j \"k\n");
}

#[test]
fn xargs_null() {
    ucmd()
        .args(&["-0n1"])
        .pipe_in("ab c\0d\tef\0")
        .succeeds()
        .stdout_only("ab c\nd\tef\n");
}

#[test]
fn xargs_delim() {
    ucmd()
        .args(&["-d1"])
        .pipe_in("ab1cd1ef")
        .succeeds()
        .stdout_only("ab cd ef\n");

    ucmd()
        .args(&["-d\\t", "-n1"])
        .pipe_in("a\nb\td e\tfg")
        .succeeds()
        .stdout_only("a\nb\nd e\nfg\n");

    ucmd()
        .args(&["-dabc"])
        .fails_with_code(1)
        .stderr_contains("invalid")
        .no_stdout();
}

#[test]
fn xargs_null_conflict() {
    ucmd()
        .args(&["-d\t", "-0n1"])
        .pipe_in("ab c\0d\tef\0")
        .succeeds()
        .stdout_only("ab c\nd\tef\n");
}

#[test]
fn xargs_if_empty() {
    // Should echo at least once still.
    ucmd().succeeds().no_stderr().stdout_only("\n");

    // Should never echo.
    ucmd().args(&["--no-run-if-empty"]).succeeds().no_output();
}

#[test]
fn xargs_replace_empty_input() {
    ucmd()
        .args(&["-I", "{}", "echo", "hello", "{}"])
        .succeeds()
        .no_output();
    ucmd().args(&["-i", "echo", "{}"]).succeeds().no_output();
}

#[test]
fn xargs_max_args() {
    ucmd()
        .args(&["-n2"])
        .pipe_in("ab cd ef\ngh i")
        .succeeds()
        .stdout_only("ab cd\nef gh\ni\n");
}

#[test]
fn xargs_max_lines() {
    for arg in ["-L2", "--max-lines=2", "-l2"] {
        ucmd()
            .arg(arg)
            .pipe_in("ab cd\nef\ngh i\n\njkl\n")
            .succeeds()
            .stdout_only("ab cd ef\ngh i jkl\n");
    }
    ucmd()
        .arg("-l")
        .pipe_in("ab cd\nef\ngh i\n\njkl\n")
        .succeeds()
        .stdout_only("ab cd\nef\ngh i\njkl\n");
}

#[test]
fn xargs_max_args_lines_conflict() {
    // -n2 is last, so it should be given priority.
    ucmd()
        .args(&["-L2", "-n2"])
        .pipe_in("ab cd ef\ngh i")
        .succeeds()
        .stderr_contains("WARNING")
        .stdout_is("ab cd\nef gh\ni\n");

    // -n2 is last, so it should be given priority.
    ucmd()
        .args(&["-I=_", "-n2", "echo", "_"])
        .pipe_in("ab   cd ef\ngh i\njkl")
        .succeeds()
        .stderr_contains("WARNING")
        .stdout_is("_ ab cd\n_ ef gh\n_ i jkl\n");

    // -L2 is last, so it should be given priority.
    ucmd()
        .args(&["-n2", "-L2"])
        .pipe_in("ab cd\nef\ngh i\n\njkl\n")
        .succeeds()
        .stderr_contains("WARNING")
        .stdout_is("ab cd ef\ngh i jkl\n");

    // -L2 is last, so it should be given priority.
    ucmd()
        .args(&["-I=_", "-L2", "echo", "_"])
        .pipe_in("ab cd\nef\ngh i\n\njkl\n")
        .succeeds()
        .stderr_contains("WARNING")
        .stdout_is("_ ab cd ef\n_ gh i jkl\n");

    for redundant_arg in ["-L2", "-n2"] {
        // -I={} is last, so it should be given priority.
        ucmd()
            .args(&[redundant_arg, "-I={}", "echo", "{} bar"])
            .pipe_in("ab  cd ef\ngh i\njkl")
            .succeeds()
            .stderr_contains("WARNING")
            .stdout_is("ab  cd ef bar\ngh i bar\njkl bar\n");
    }
}

#[test]
fn xargs_max_chars() {
    for arg in ["-s11", "--max-chars=11"] {
        ucmd()
            .arg(arg)
            .pipe_in("ab cd efg")
            .succeeds()
            .stdout_only("ab cd\nefg\n");
    }

    // Behavior should be the same with -x, which only takes effect with -L or
    // -n.
    ucmd()
        .args(&["-xs11"])
        .pipe_in("ab cd efg")
        .succeeds()
        .stdout_only("ab cd\nefg\n");

    ucmd()
        .args(&["-s10"])
        .pipe_in("abcdefghijkl ab")
        .fails_with_code(1)
        .stderr_contains("Error:")
        .no_stdout();
}

#[test]
fn xargs_exit_on_large() {
    ucmd()
        .args(&["-xs11", "-n2"])
        .pipe_in("ab cd efg h i")
        .succeeds()
        .stdout_only("ab cd\nefg h\ni\n");

    ucmd()
        .args(&["-xs11", "-n2"])
        .pipe_in("abcdefg hijklmn")
        .fails_with_code(1)
        .stderr_contains("Error:")
        .no_stdout();
}

#[test]
#[cfg(unix)]
fn xargs_batches_large_input_below_system_limit() {
    // Arguments totalling more than ARG_MAX must be split into several
    // command invocations instead of failing with E2BIG.
    let arg_count = 60_000;
    let input = (0..arg_count)
        .map(|i| format!("/some/path/to/a/file/with/a/long/name-{i:06}"))
        .collect::<Vec<_>>()
        .join("\n");

    let result = ucmd().arg("echo").pipe_in(input).succeeds();
    assert_eq!(
        arg_count,
        result.stdout_str().split_ascii_whitespace().count()
    );
}

#[test]
#[cfg(target_os = "linux")]
fn xargs_explicit_size_can_exceed_default_cap() {
    // An explicit -s larger than the 128 KiB default cap must be honored:
    // ~200 KiB of arguments fit into a single invocation with -s 300000.
    let arg_count = 4_000;
    let input = (0..arg_count)
        .map(|i| format!("/quite/a/long/path/segment/for/padding-{i:06}"))
        .collect::<Vec<_>>()
        .join("\n");

    let result = ucmd()
        .args(&["-s", "300000", "echo"])
        .pipe_in(input)
        .succeeds();
    assert_eq!(1, result.stdout_str().lines().count());
    assert_eq!(
        arg_count,
        result.stdout_str().split_ascii_whitespace().count()
    );
}

#[test]
fn xargs_exec() {
    let result = ucmd()
        .args(&[
            "-n2",
            &path_to_testing_commandline(),
            "-",
            "--print_stdin",
            "--no_print_cwd",
        ])
        .pipe_in("a b c\nd")
        .succeeds();
    result.no_stderr();
    assert_eq!(
        result.stdout_str(),
        "stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\
            stdin=\nargs=\n--print_stdin\n--no_print_cwd\nc\nd\n",
    );
}

#[test]
fn xargs_exec_stdin_open() {
    let temp_file = tempfile::NamedTempFile::new().unwrap();
    std::fs::write(temp_file.path(), b"a b c").unwrap();

    let result = ucmd()
        .args(&[
            "-a",
            &temp_file.path().to_string_lossy(),
            &path_to_testing_commandline(),
            "-",
            "--print_stdin",
            "--no_print_cwd",
        ])
        .pipe_in("test")
        .succeeds();
    result.no_stderr();
    assert_eq!(
        result.stdout_str(),
        "stdin=test\nargs=\n--print_stdin\n--no_print_cwd\na\nb\nc\n",
    );
}

#[test]
fn xargs_exec_failure() {
    let result = ucmd()
        .args(&[
            "-n1",
            &path_to_testing_commandline(),
            "-",
            "--no_print_cwd",
            "--exit_with_failure",
        ])
        .pipe_in("a b")
        .run();
    result.code_is(123);
    result.no_stderr();
    assert_eq!(
        result.stdout_str(),
        "args=\n--no_print_cwd\n--exit_with_failure\na\n\
                args=\n--no_print_cwd\n--exit_with_failure\nb\n",
    );
}

#[test]
fn xargs_exec_urgent_failure() {
    let result = ucmd()
        .args(&[
            "-n1",
            &path_to_testing_commandline(),
            "-",
            "--no_print_cwd",
            "--exit_with_urgent_failure",
        ])
        .pipe_in("a b")
        .run();
    result.code_is(124);
    assert!(
        !result.stderr_str().is_empty(),
        "stderr should not be empty"
    );
    assert_eq!(
        result.stdout_str(),
        "args=\n--no_print_cwd\n--exit_with_urgent_failure\na\n"
    );
}

#[test]
#[cfg(unix)]
fn xargs_exec_with_signal() {
    let result = ucmd()
        .args(&[
            "-n1",
            &path_to_testing_commandline(),
            "-",
            "--no_print_cwd",
            "--exit_with_signal",
        ])
        .pipe_in("a b")
        .run();
    result.code_is(125);
    assert!(
        !result.stderr_str().is_empty(),
        "stderr should not be empty"
    );
    assert_eq!(
        result.stdout_str(),
        "args=\n--no_print_cwd\n--exit_with_signal\na\n"
    );
}

#[test]
fn xargs_exec_not_found() {
    ucmd()
        .args(&["this-file-does-not-exist"])
        .fails_with_code(127)
        .stderr_contains("Error:")
        .no_stdout();
}

#[test]
fn xargs_exec_verbose() {
    ucmd()
        .args(&[
            "-n2",
            "--verbose",
            &path_to_testing_commandline(),
            "-",
            "--print_stdin",
            "--no_print_cwd",
        ])
        .pipe_in("a b c\nd")
        .succeeds()
        .stderr_contains("testing-commandline")
        .stdout_is(
            "stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\
            stdin=\nargs=\n--print_stdin\n--no_print_cwd\nc\nd\n",
        );
}

#[test]
fn xargs_unterminated_quote() {
    ucmd()
        .args(&[
            "-n2",
            &path_to_testing_commandline(),
            "-",
            "--print_stdin",
            "--no_print_cwd",
        ])
        .pipe_in("a \"b c\nd")
        .fails_with_code(1)
        .stderr_contains("Error: Unterminated quote:")
        .no_stdout();
}

#[test]
fn xargs_zero_lines() {
    ucmd()
        .args(&[
            "-L0",
            &path_to_testing_commandline(),
            "-",
            "--print_stdin",
            "--no_print_cwd",
        ])
        // Empty stdin: -L0 is rejected before reading input, so writing
        // actual content races with xargs exiting and causes a broken pipe.
        .pipe_in("")
        .fails_with_code(1)
        .stderr_contains("Value must be > 0, not: 0")
        .no_stdout();
}

#[test]
fn xargs_replace() {
    ucmd()
        .args(&["-i={}", "echo", "{} bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo bar");

    ucmd()
        .args(&["-i=_", "echo", "_ bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo bar");

    ucmd()
        .args(&["--replace=_", "echo", "_ _ bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo foo bar");

    ucmd()
        .args(&["-i=_", "echo", "_ _ bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo foo bar");

    ucmd()
        .args(&["-i", "echo", "{} {} bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo foo bar");

    ucmd()
        .args(&["-I={}", "echo", "{} bar {}"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo bar foo");

    // Combine the two options to see which one wins
    ucmd()
        .args(&["-I=_", "-i", "echo", "{} bar {}"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo bar foo");

    // other order
    ucmd()
        .args(&["-i", "-I=_", "echo", "{} bar {}"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("{} bar {}");

    ucmd()
        .args(&["-i", "-I", "_", "echo", "{} bar _"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("{} bar foo");

    ucmd()
        .args(&["-I", "-_", "echo", "-_ bar"])
        .pipe_in("foo")
        .succeeds()
        .stdout_contains("foo bar");

    // Expected to fail
    ucmd()
        .args(&["-I", "echo", "_ _ bar"])
        .pipe_in("foo")
        .fails()
        .stderr_contains("Error: Command not found");
}

#[test]
fn xargs_replace_multiple_lines() {
    ucmd()
        .args(&["-I", "_", "echo", "[_]"])
        .pipe_in("ab c\nd  ef\ng")
        .succeeds()
        .stdout_only("[ab c]\n[d  ef]\n[g]\n");

    ucmd()
        .args(&["-I", "{}", "echo", "{} {} foo"])
        .pipe_in("bar\nbaz")
        .succeeds()
        .stdout_only("bar bar foo\nbaz baz foo\n");

    ucmd()
        .args(&["-I", "non-exist", "echo"])
        .pipe_in("abc\ndef\ng")
        .succeeds()
        .stdout_only("\n\n\n");
}

#[test]
fn xargs_help() {
    for option_style in ["-h", "--help"] {
        ucmd()
            .args(&[option_style])
            .succeeds()
            .no_stderr()
            .stdout_contains("--help");
    }
}

// Do not regress to:
//
// ❯ xargs --version
// Error: xargs 0.7.0
//
// Same for help above.
#[test]
fn xargs_version() {
    for option_style in ["-V", "--version"] {
        let output = ucmd().args(&[option_style]).succeeds();
        let result = output.no_stderr();
        assert!(
            result.stdout_str().starts_with("xargs "),
            "expected stdout to start with 'xargs ', got: {:?}",
            result.stdout_str()
        );
    }
}

#[test]
fn xargs_eof() {
    for option_style in [vec!["-ecd"], vec!["-E", "cd"], vec!["--eof", "cd"]] {
        ucmd()
            .args(option_style.as_slice())
            .pipe_in("ab cd ef")
            .succeeds()
            .stdout_only("ab\n");
    }

    ucmd()
        .args(&["-E", "-end"])
        .pipe_in("ab -end ef")
        .succeeds()
        .stdout_only("ab\n");
}

#[test]
fn xargs_eof_with_delimiter() {
    ucmd()
        .args(&["-0", "-Ecd"])
        .pipe_in("ab\0cd\0ef")
        .succeeds()
        .stdout_only("ab cd ef\n");
}
