// 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) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile

use rstest::rstest;
use rstest_reuse::{apply, template};

use uutests::at_and_ucmd;
use uutests::new_ucmd;
use uutests::util::TestScenario;
use uutests::util::UCommand;
use uutests::util::log_info;
use uutests::util_name;

#[template]
#[rstest]
#[case::sysv("sysv")]
#[case::bsd("bsd")]
#[case::crc("crc")]
#[case::md5("md5")]
#[case::sha1("sha1")]
#[case::sha224("sha224")]
#[case::sha256("sha256")]
#[case::sha384("sha384")]
#[case::sha512("sha512")]
#[case::blake2b("blake2b")]
#[case::blake3("blake3")]
#[case::sm3("sm3")]
fn test_all_algos(#[case] algo: &str) {}

#[template]
#[rstest]
fn test_sha(#[values("sha2", "sha3")] algo: &str, #[values(224, 256, 384, 512)] len: u32) {}

fn sha_fixture_name(algo: &str, len: u32, prefix: &str, suffix: &str) -> String {
    // assume algo is always "sha2" or "sha3"
    match algo {
        "sha2" => format!("{prefix}sha{len}{suffix}"),
        _ => format!("{prefix}sha3_{len}{suffix}"),
    }
}

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

#[test]
fn test_single_file() {
    new_ucmd!()
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("crc_single_file.expected");
}

#[test]
fn test_multiple_files() {
    new_ucmd!()
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture("crc_multiple_files.expected");
}

#[test]
fn test_stdin() {
    new_ucmd!()
        .pipe_in_fixture("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("crc_stdin.expected");
}

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

    at.touch("a");

    ucmd.arg("a")
        .succeeds()
        .no_stderr()
        .normalized_newlines_stdout_is("4294967295 0 a\n");
}

#[test]
fn test_arg_overrides_stdin() {
    let (at, mut ucmd) = at_and_ucmd!();
    let input = "foobarfoobar"; // spell-checker:disable-line

    at.touch("a");

    ucmd.arg("a")
        .pipe_in(input.as_bytes())
        // the command might have exited before all bytes have been pipe in.
        // in that case, we don't care about the error (broken pipe)
        .ignore_stdin_write_error()
        .succeeds()
        .no_stderr()
        .normalized_newlines_stdout_is("4294967295 0 a\n");
}

#[test]
fn test_nonexisting_file() {
    let file_name = "asdf";

    new_ucmd!()
        .arg(file_name)
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(format!("cksum: {file_name}: No such file or directory"));
}

#[test]
fn test_nonexisting_file_out() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write(
        "f",
        "MD5 (nonexistent) = e5773576fc75ff0f8eba14f61587ae28\n",
    );

    ucmd.arg("-c")
        .arg("f")
        .fails()
        .stdout_contains("nonexistent: FAILED open or read")
        .stderr_contains("cksum: nonexistent: No such file or directory");
}

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

    at.touch("abc.txt");
    at.touch("xyz.txt");

    ucmd.arg("abc.txt")
        .arg("asdf.txt")
        .arg("xyz.txt")
        .fails()
        .stdout_contains_line("4294967295 0 xyz.txt")
        .stderr_contains("asdf.txt: No such file or directory")
        .stdout_contains_line("4294967295 0 abc.txt");
}

// Make sure crc is correct for files larger than 32 bytes
// but <128 bytes (1 fold pclmul) // spell-checker:disable-line
#[test]
fn test_crc_for_bigger_than_32_bytes() {
    let result = new_ucmd!().arg("chars.txt").succeeds();

    let mut stdout_split = result.stdout_str().split(' ');

    let cksum: i64 = stdout_split.next().unwrap().parse().unwrap();
    let bytes_cnt: i64 = stdout_split.next().unwrap().parse().unwrap();

    assert_eq!(cksum, 586_047_089);
    assert_eq!(bytes_cnt, 16);
}

#[test]
fn test_stdin_larger_than_128_bytes() {
    let result = new_ucmd!().arg("larger_than_2056_bytes.txt").succeeds();

    let mut stdout_split = result.stdout_str().split(' ');

    let cksum: i64 = stdout_split.next().unwrap().parse().unwrap();
    let bytes_cnt: i64 = stdout_split.next().unwrap().parse().unwrap();

    assert_eq!(cksum, 945_881_979);
    assert_eq!(bytes_cnt, 2058);
}

#[test]
fn test_repeated_flags() {
    new_ucmd!()
        .arg("-a")
        .arg("sha1")
        .arg("--algo=sha256")
        .arg("-a=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("md5_single_file.expected");
}

#[test]
fn test_tag_after_untagged() {
    new_ucmd!()
        .arg("--untagged")
        .arg("--tag")
        .arg("-a=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("md5_single_file.expected");
}

#[apply(test_all_algos)]
fn test_algorithm_single_file(#[case] algo: &str) {
    for option in ["-a", "--algorithm"] {
        new_ucmd!()
            .arg(format!("{option}={algo}"))
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stdout_is_fixture(format!("{algo}_single_file.expected"));
    }
}

#[apply(test_all_algos)]
fn test_algorithm_multiple_files(#[case] algo: &str) {
    for option in ["-a", "--algorithm"] {
        new_ucmd!()
            .arg(format!("{option}={algo}"))
            .arg("lorem_ipsum.txt")
            .arg("alice_in_wonderland.txt")
            .succeeds()
            .stdout_is_fixture(format!("{algo}_multiple_files.expected"));
    }
}

#[apply(test_all_algos)]
fn test_algorithm_stdin(#[case] algo: &str) {
    for option in ["-a", "--algorithm"] {
        new_ucmd!()
            .arg(format!("{option}={algo}"))
            .pipe_in_fixture("lorem_ipsum.txt")
            .succeeds()
            .stdout_is_fixture(format!("{algo}_stdin.expected"));
    }
}

#[test]
fn test_untagged_single_file() {
    new_ucmd!()
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("untagged/crc_single_file.expected");
}

#[test]
fn test_untagged_multiple_files() {
    new_ucmd!()
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture("untagged/crc_multiple_files.expected");
}

#[test]
fn test_untagged_stdin() {
    new_ucmd!()
        .arg("--untagged")
        .pipe_in_fixture("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("untagged/crc_stdin.expected");
}

#[apply(test_all_algos)]
fn test_untagged_algorithm_single_file(#[case] algo: &str) {
    new_ucmd!()
        .arg("--untagged")
        .arg(format!("--algorithm={algo}"))
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(format!("untagged/{algo}_single_file.expected"));
}

#[test]
fn test_tag_short() {
    new_ucmd!()
        .arg("-t")
        .arg("--untagged")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa  lorem_ipsum.txt\n");
}

#[test]
fn test_untagged_algorithm_after_tag() {
    new_ucmd!()
        .arg("--tag")
        .arg("--untagged")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("untagged/md5_single_file.expected");
}

#[apply(test_all_algos)]
fn test_untagged_algorithm_multiple_files(#[case] algo: &str) {
    new_ucmd!()
        .arg("--untagged")
        .arg(format!("--algorithm={algo}"))
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected"));
}

#[apply(test_all_algos)]
fn test_untagged_algorithm_stdin(#[case] algo: &str) {
    new_ucmd!()
        .arg("--untagged")
        .arg(format!("--algorithm={algo}"))
        .pipe_in_fixture("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(format!("untagged/{algo}_stdin.expected"));
}

#[test]
fn test_sha_length_invalid() {
    for algo in ["sha2", "sha3"] {
        for l in ["0", "00", "13", "56", "99999999999999999999999999"] {
            new_ucmd!()
                .arg("--algorithm")
                .arg(algo)
                .arg("--length")
                .arg(l)
                .arg("/dev/null")
                .fails_with_code(1)
                .no_stdout()
                .stderr_contains(format!("invalid length: '{l}'"))
                .stderr_contains(format!(
                    "digest length for '{}' must be 224, 256, 384, or 512",
                    algo.to_ascii_uppercase()
                ));

            // Also fails with --check
            new_ucmd!()
                .arg("--algorithm")
                .arg(algo)
                .arg("--length")
                .arg(l)
                .arg("/dev/null")
                .arg("--check")
                .fails_with_code(1)
                .no_stdout()
                .stderr_contains(format!("invalid length: '{l}'"))
                .stderr_contains(format!(
                    "digest length for '{}' must be 224, 256, 384, or 512",
                    algo.to_ascii_uppercase()
                ));
        }

        // Different error for NaNs
        for l in ["512x", "x512", "512x512"] {
            new_ucmd!()
                .arg("--algorithm")
                .arg(algo)
                .arg("--length")
                .arg(l)
                .arg("/dev/null")
                .fails_with_code(1)
                .no_stdout()
                .stderr_contains(format!("invalid length: '{l}'"));

            // Also fails with --check
            new_ucmd!()
                .arg("--algorithm")
                .arg(algo)
                .arg("--length")
                .arg(l)
                .arg("/dev/null")
                .arg("--check")
                .fails_with_code(1)
                .no_stdout()
                .stderr_contains(format!("invalid length: '{l}'"));
        }
    }
}

#[test]
fn test_sha_missing_length() {
    for algo in ["sha2", "sha3"] {
        new_ucmd!()
            .arg("--algorithm")
            .arg(algo)
            .arg("lorem_ipsum.txt")
            .fails_with_code(1)
            .no_stdout()
            .stderr_contains(format!(
                "--algorithm={algo} requires specifying --length 224, 256, 384, or 512"
            ));
    }
}

fn sha_cmd(algo: &str, len: u32) -> UCommand {
    let mut ucmd = new_ucmd!();
    ucmd.arg(format!("--algorithm={algo}"))
        .arg(format!("--length={len}"));
    ucmd
}

#[apply(test_sha)]
fn test_sha_single_file(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(algo, len, "", "_single_file.expected"));
}

#[apply(test_sha)]
fn test_sha_multiple_files(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(algo, len, "", "_multiple_files.expected"));
}

#[apply(test_sha)]
fn test_sha_stdin(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .pipe_in_fixture("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(algo, len, "", "_stdin.expected"));
}

#[apply(test_sha)]
fn test_untagged_sha_single_file(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(
            algo,
            len,
            "untagged/",
            "_single_file.expected",
        ));
}

#[apply(test_sha)]
fn test_untagged_sha_multiple_files(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(
            algo,
            len,
            "untagged/",
            "_multiple_files.expected",
        ));
}

#[apply(test_sha)]
fn test_untagged_sha_stdin(algo: &str, len: u32) {
    sha_cmd(algo, len)
        .arg("--untagged")
        .pipe_in_fixture("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture(sha_fixture_name(algo, len, "untagged/", "_stdin.expected"));
}

#[apply(test_sha)]
fn test_check_tagged_sha_single_file(algo: &str, len: u32) {
    new_ucmd!()
        .arg("--check")
        .arg(sha_fixture_name(algo, len, "", "_single_file.expected"))
        .succeeds()
        .stdout_is("lorem_ipsum.txt: OK\n");
}

#[apply(test_sha)]
fn test_check_tagged_sha_multiple_files(algo: &str, len: u32) {
    new_ucmd!()
        .arg("--check")
        .arg(sha_fixture_name(algo, len, "", "_multiple_files.expected"))
        .succeeds()
        .stdout_contains("lorem_ipsum.txt: OK\n")
        .stdout_contains("alice_in_wonderland.txt: OK\n");
}

// When checking sha2/sha3 in untagged mode, the length is automatically
// deduced from the length of the digest.
#[apply(test_sha)]
fn test_check_untagged_sha_single_file(algo: &str, len: u32) {
    new_ucmd!()
        .arg("--check")
        .arg(format!("--algorithm={algo}"))
        .arg(sha_fixture_name(
            algo,
            len,
            "untagged/",
            "_single_file.expected",
        ))
        .succeeds()
        .stdout_is("lorem_ipsum.txt: OK\n");
}

#[apply(test_sha)]
fn test_check_untagged_sha_multiple_files(algo: &str, len: u32) {
    new_ucmd!()
        .arg("--check")
        .arg(format!("--algorithm={algo}"))
        .arg(sha_fixture_name(
            algo,
            len,
            "untagged/",
            "_multiple_files.expected",
        ))
        .succeeds()
        .stdout_contains("lorem_ipsum.txt: OK\n")
        .stdout_contains("alice_in_wonderland.txt: OK\n");
}

#[rstest]
#[case::sha2("sha2", &[
    "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f",
    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
    "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
])]
#[case::sha3("sha3", &[
    "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7",
    "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
    "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004",
    "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
])]
fn test_check_untagged_sha_invalid_length(#[case] algo: &str, #[case] digests: &[&str; 4]) {
    // When checking with --algorithm sha2/sha3, Guess the length from the provided
    // digest. Raise "improperly formatted" if the digest is not af adequate
    // size (224, 256, 384 or 512 bits).

    let (at, mut ucmd) = at_and_ucmd!();
    at.touch("a");
    at.touch("b");
    at.touch("c");
    at.touch("d");
    at.touch("e");

    let invalid = "xxxx  e";

    ucmd.arg("-a")
        .arg(algo)
        .arg("-c")
        .pipe_in(format!(
            "{}  a\n{}  b\n{}  c\n{}  d\n{invalid}",
            digests[0], digests[1], digests[2], digests[3]
        ))
        .succeeds()
        .stdout_contains("a: OK")
        .stdout_contains("b: OK")
        .stdout_contains("c: OK")
        .stdout_contains("d: OK")
        .stdout_does_not_contain("e: FAILED")
        .stderr_contains("improperly formatted");
}

#[test]
fn test_check_sha2_tagged_variant() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    at.touch("f");

    // SHA2-xxx is an alias to SHAxxx we don't output but we still recognize.
    let checksum_lines = [
        (
            "SHA224",
            "SHA2-224",
            "(f) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f",
        ),
        (
            "SHA256",
            "SHA2-256",
            "(f) = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        ),
        (
            "SHA384",
            "SHA2-384",
            "(f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
        ),
        (
            "SHA512",
            "SHA2-512",
            "(f) = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
        ),
    ];

    for (basic, variant, digest) in checksum_lines {
        let stdin = format!("{basic} {digest}");
        log_info("stdin is: ", &stdin);
        scene
            .ucmd()
            .arg("--check")
            .arg("--algorithm=sha2")
            .pipe_in(stdin)
            .succeeds()
            .stdout_is("f: OK\n");

        // Check that the variant works the same
        let stdin = format!("{variant} {digest}");
        log_info("stdin is: ", &stdin);
        scene
            .ucmd()
            .arg("--check")
            .arg("--algorithm=sha2")
            .pipe_in(stdin)
            .succeeds()
            .stdout_is("f: OK\n");
    }
}

#[test]
fn test_check_sha2_tagged_missing_hint() {
    // SHA2 (<file>) = <digest>
    //
    // should fail and raise "improperly formatted" because SHA2 expects a
    // mandatory length hint.

    let (at, mut ucmd) = at_and_ucmd!();
    at.touch("a");
    at.touch("b");

    // valid digest but missing hint
    let invalid_sha224 = "SHA2 (a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f";
    // invalid digest
    let invalid = "SHA2 (b) = xxxx";

    ucmd.arg("-c")
        .pipe_in(format!("{invalid_sha224}\n{invalid}"))
        .fails()
        .stderr_contains("no properly formatted checksum lines found");
}

#[test]
fn test_check_tagged_missing_algo() {
    // When checking tagged lines, if the algorithm is missing, print an "improperly
    // formatted" message rather than panic.

    let (at, mut ucmd) = at_and_ucmd!();
    at.touch("a");

    let invalid1 = "(a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f";
    let invalid2 = "a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f";
    let invalid3 = "(a = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f";

    ucmd.arg("-c")
        .pipe_in(format!("{invalid1}\n{invalid2}\n{invalid3}"))
        .fails()
        .stderr_contains("no properly formatted checksum lines found");
}

#[rstest]
#[case::md5("md5", "d41d8cd98f00b204e9800998ecf8427e")]
#[case::sha1("sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709")]
#[case::sha224("sha224", "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f")]
#[case::sha256(
    "sha256",
    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)]
#[case::sha384(
    "sha384",
    "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
)]
#[case::sha512(
    "sha512",
    "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
)]
#[case::sm3(
    "sm3",
    "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"
)]
fn test_check_untagged_with_invalid_length(#[case] algo: &str, #[case] digest: &str) {
    // issue #11202

    // Ensures that when checking untagged lines, if the provided algorithm is
    // one of `sha(224|256|384|512)`, digests whose length mismatches the
    // length of the given algorithm will report as "improperly formatted"
    // rather than a  FAILED check.

    let (at, mut ucmd) = at_and_ucmd!();
    at.touch("a");
    at.touch("b");

    let good_checksum = format!("{digest}  a");
    let invalid_checksum = "e3b0  b";

    ucmd.arg("-a")
        .arg(algo)
        .arg("-c")
        .pipe_in(format!("{invalid_checksum}\n{good_checksum}"))
        .succeeds()
        .stdout_contains("a: OK")
        .stdout_does_not_contain("b: FAILED")
        .stderr_contains("WARNING: 1 line is improperly formatted");
}

#[test]
fn test_check_algo() {
    for algo in ["bsd", "sysv", "crc", "crc32b"] {
        new_ucmd!()
            .arg("-a")
            .arg(algo)
            .arg("--check")
            .arg("lorem_ipsum.txt")
            .fails()
            .no_stdout()
            .stderr_contains(
                "cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}",
            );
    }
}

#[test]
fn test_length_with_wrong_algorithm() {
    new_ucmd!()
        .arg("--length=16")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(
            "cksum: --length is only supported with --algorithm blake2b, sha2, or sha3",
        );

    new_ucmd!()
        .arg("--length=16")
        .arg("--algorithm=md5")
        .arg("-c")
        .arg("foo.sums")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(
            "cksum: --length is only supported with --algorithm blake2b, sha2, or sha3",
        );
}

/// Giving --length to a wrong algorithm doesn't fail if the length is zero
#[test]
fn test_length_is_zero_with_wrong_algorithm() {
    for algo in ["md5", "crc", "sha1", "sha224", "sha256", "sha384", "sha512"] {
        new_ucmd!()
            .arg("--length=0")
            .args(&["-a", algo])
            .arg("lorem_ipsum.txt")
            .succeeds()
            .no_stderr()
            .stdout_is_fixture(format!("{algo}_single_file.expected"));
    }
}

#[test]
fn test_length_not_supported() {
    new_ucmd!()
        .arg("--length=15")
        .arg("lorem_ipsum.txt")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(
            "cksum: --length is only supported with --algorithm blake2b, sha2, or sha3",
        );

    new_ucmd!()
        .arg("-l")
        .arg("158")
        .arg("-c")
        .arg("-a")
        .arg("crc")
        .arg("/tmp/xxx")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(
            "cksum: --length is only supported with --algorithm blake2b, sha2, or sha3",
        );
}

#[test]
fn test_blake2b_length() {
    new_ucmd!()
        .arg("--length=16")
        .arg("--algorithm=blake2b")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_contains(
            "BLAKE2b-16 (lorem_ipsum.txt) = 7e2f\nBLAKE2b-16 (alice_in_wonderland.txt) = a546",
        );
}

#[test]
fn test_blake2b_length_greater_than_512() {
    for l in ["513", "1024", "73786976294838206464"] {
        new_ucmd!()
            .arg("--algorithm=blake2b")
            .arg("--length")
            .arg(l)
            .arg("lorem_ipsum.txt")
            .fails_with_code(1)
            .no_stdout()
            .stderr_contains(format!("invalid length: '{l}'"))
            .stderr_contains("maximum digest length for 'BLAKE2b' is 512 bits");
    }
}

#[test]
fn test_blake2b_length_nan() {
    for l in ["foo", "512x", "x512", "0xff"] {
        new_ucmd!()
            .arg("--algorithm=blake2b")
            .arg("--length")
            .arg(l)
            .arg("lorem_ipsum.txt")
            .fails_with_code(1)
            .no_stdout()
            .stderr_contains(format!("invalid length: '{l}'"));
    }
}

#[test]
fn test_blake2b_length_is_zero() {
    new_ucmd!()
        .arg("--length=0")
        .arg("--algorithm=blake2b")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .no_stderr()
        .stdout_is_fixture("length_is_zero.expected");
}

#[test]
fn test_blake2b_length_repeated() {
    new_ucmd!()
        .arg("--length=10")
        .arg("--length=123456")
        .arg("--length=0")
        .arg("--algorithm=blake2b")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .no_stderr()
        .stdout_is_fixture("length_is_zero.expected");
}

#[test]
fn test_blake2b_length_invalid() {
    for len in [
        "1", "01", // Odd
        "",
    ] {
        new_ucmd!()
            .arg("--length")
            .arg(len)
            .arg("--algorithm=blake2b")
            .arg("lorem_ipsum.txt")
            .arg("alice_in_wonderland.txt")
            .fails_with_code(1)
            .stderr_contains(format!("invalid length: '{len}'"));
    }
}

#[apply(test_all_algos)]
fn test_raw_single_file(#[case] algo: &str) {
    new_ucmd!()
        .arg("--raw")
        .arg("lorem_ipsum.txt")
        .arg(format!("--algorithm={algo}"))
        .succeeds()
        .no_stderr()
        .stdout_is_fixture_bytes(format!("raw/{algo}_single_file.expected"));
}

#[test]
fn test_raw_multiple_files() {
    new_ucmd!()
        .arg("--raw")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains("cksum: the --raw option is not supported with multiple files");
}

#[test]
fn test_base64_raw_conflicts() {
    new_ucmd!()
        .arg("--base64")
        .arg("--raw")
        .arg("lorem_ipsum.txt")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains("--base64")
        .stderr_contains("cannot be used with")
        .stderr_contains("--raw");
}

#[apply(test_all_algos)]
fn test_base64_single_file(#[case] algo: &str) {
    new_ucmd!()
        .arg("--base64")
        .arg("lorem_ipsum.txt")
        .arg(format!("--algorithm={algo}"))
        .succeeds()
        .no_stderr()
        .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected"));
}

#[test]
fn test_base64_multiple_files() {
    new_ucmd!()
        .arg("--base64")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .no_stderr()
        .stdout_is_fixture_bytes("base64/md5_multiple_files.expected");
}

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

    let folder_name = "a_folder";
    at.mkdir(folder_name);

    ucmd.arg(folder_name)
        .fails()
        .no_stdout()
        .stderr_contains(format!("cksum: {folder_name}: Is a directory"));
}

#[apply(test_all_algos)]
fn test_all_algorithms_fail_on_folder(#[case] algo: &str) {
    let scene = TestScenario::new(util_name!());

    let at = &scene.fixtures;

    let folder_name = "a_folder";
    at.mkdir(folder_name);

    scene
        .ucmd()
        .arg(format!("--algorithm={algo}"))
        .arg(folder_name)
        .fails()
        .no_stdout()
        .stderr_contains(format!("cksum: {folder_name}: Is a directory"));
}

#[cfg(unix)]
#[test]
fn test_check_error_incorrect_format() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    at.write("checksum", "e5773576fc75ff0f8eba14f61587ae28  README.md");

    scene
        .ucmd()
        .arg("-c")
        .arg("checksum")
        .fails()
        .stderr_contains("no properly formatted checksum lines found");
}

#[cfg(unix)]
#[test]
fn test_dev_null() {
    let scene = TestScenario::new(util_name!());

    scene
        .ucmd()
        .arg("--tag")
        .arg("--untagged")
        .arg("--algorithm=md5")
        .arg("/dev/null")
        .succeeds()
        .stdout_contains("d41d8cd98f00b204e9800998ecf8427e ");
}

#[cfg(unix)]
#[test]
fn test_blake2b_default_length() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.touch("f");

    scene
        .ucmd()
        .arg("-a")
        .arg("blake2b")
        .arg("-l512")
        .arg("f")
        .succeeds()
        .stdout_contains("BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce");

    // test also the read
    at.write("checksum", "BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce");
    scene
        .ucmd()
        .arg("--check")
        .arg("checksum")
        .succeeds()
        .stdout_contains("f: OK");

    scene
        .ucmd()
        .arg("--status")
        .arg("--check")
        .arg("checksum")
        .succeeds()
        .no_output();
}

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

    at.touch("f");

    scene
        .ucmd()
        .arg("--binary")
        .arg("--tag")
        .arg("--untagged")
        .arg("--algorithm=md5")
        .arg(at.subdir.join("f"))
        .succeeds()
        .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *");
}

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

    at.touch("f");

    scene
        .ucmd()
        .arg("--binary")
        .arg("--tag")
        .arg("--untagged")
        .arg("--binary")
        .arg("--algorithm=md5")
        .arg(at.subdir.join("f"))
        .succeeds()
        .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *");
}

mod output_format {
    use super::*;

    #[test]
    fn test_text_tag() {
        let (at, mut ucmd) = at_and_ucmd!();
        at.touch("f");

        ucmd.arg("--text")
            .arg("--tag")
            .args(&["-a", "md5"])
            .arg(at.subdir.join("f"))
            .fails();
    }

    #[test]
    fn test_text_no_untagged() {
        let (at, mut ucmd) = at_and_ucmd!();
        at.touch("f");

        // --text without --untagged fails
        ucmd.arg("--text")
            .args(&["-a", "md5"])
            .arg(at.subdir.join("f"))
            .fails_with_code(1)
            .stderr_contains("--text mode is only supported with --untagged");
    }

    #[test]
    fn test_text_binary() {
        let (at, mut ucmd) = at_and_ucmd!();
        at.touch("f");

        // --binary overwrites --text, thus no error is raised
        ucmd.arg("--text")
            .arg("--binary")
            .args(&["-a", "md5"])
            .arg(at.subdir.join("f"))
            .succeeds()
            // No --untagged, tagged output is used
            .stdout_contains("f) = d41d8cd98f00b204e9800998ecf8427e");
    }

    #[test]
    fn test_text_binary_untagged() {
        let (at, mut ucmd) = at_and_ucmd!();
        at.touch("f");

        // --binary overwrites --text
        ucmd.arg("--text")
            .arg("--binary")
            .arg("--untagged")
            .args(&["-a", "md5"])
            .arg(at.subdir.join("f"))
            .succeeds()
            // Untagged output is used
            .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *");
    }
}

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

    at.touch("f");

    scene
        .ucmd()
        .arg("--untagged")
        .arg("-b")
        .arg("--algorithm=md5")
        .arg(at.subdir.join("f"))
        .succeeds()
        .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *");

    scene
        .ucmd()
        .arg("--tag")
        .arg("--untagged")
        .arg("--binary")
        .arg("--algorithm=md5")
        .arg(at.subdir.join("f"))
        .succeeds()
        .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *");

    scene
        .ucmd()
        .arg("--tag")
        .arg("--untagged")
        .arg("--binary")
        .arg("--algorithm=md5")
        .arg("raw/blake2b_single_file.expected")
        .succeeds()
        .stdout_contains("7e297c07ed8e053600092f91bdd1dad7 *");

    new_ucmd!()
        .arg("--tag")
        .arg("--untagged")
        .arg("--binary")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa *lorem_ipsum.txt\n");

    new_ucmd!()
        .arg("--untagged")
        .arg("--binary")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa *lorem_ipsum.txt\n");

    new_ucmd!()
        .arg("--binary")
        .arg("--untagged")
        .arg("--algorithm=md5")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa *lorem_ipsum.txt\n");

    new_ucmd!()
        .arg("-a")
        .arg("md5")
        .arg("--binary")
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa *lorem_ipsum.txt\n");

    new_ucmd!()
        .arg("-a")
        .arg("md5")
        .arg("--binary")
        .arg("--tag")
        .arg("--untagged")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is("cd724690f7dc61775dfac400a71f2caa *lorem_ipsum.txt\n");
}

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

    let at = &scene.fixtures;

    let folder_name = "a_folder";
    at.mkdir(folder_name);

    scene
        .ucmd()
        .arg(folder_name)
        .arg("lorem_ipsum.txt")
        .fails()
        .stderr_contains(format!("cksum: {folder_name}: Is a directory"))
        .stdout_is_fixture("crc_single_file.expected");
}

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

    let at = &scene.fixtures;

    at.touch("f");

    scene
        .ucmd()
        .arg("--binary")
        .arg("--check")
        .arg("f")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains(
            "cksum: the --binary and --text options are meaningless when verifying checksums",
        );

    scene
        .ucmd()
        .arg("--tag")
        .arg("-c")
        .arg("-a")
        .arg("md5")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains("cksum: the --tag option is meaningless when verifying checksums");
}

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

    let at = &scene.fixtures;

    at.touch("f");

    scene
        .ucmd()
        .arg("-a")
        .arg("sm3")
        .arg("--check")
        .arg("f")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains("cksum: f: no properly formatted checksum lines found");
}

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

    let at = &scene.fixtures;

    at.touch("f");

    scene
        .ucmd()
        .arg("--check")
        .arg("-")
        .pipe_in("f")
        .fails_with_code(1)
        .no_stdout()
        .stderr_contains("cksum: 'standard input': no properly formatted checksum lines found");
}

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

    at.touch("f");
    at.write("CHECKSUM", "\
    SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\
    BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\
    BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\
    SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n\n");
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_does_not_contain("line is improperly formatted");
}

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

    at.touch("f");
    at.write("CHECKSUM", "\
    SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\
    BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\
    BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\
    SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n  \n");
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_contains("line is improperly formatted");
}

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

    at.touch("f");
    at.write("CHECKSUM", "\
     \\SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\
     \\BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\
     \\BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\
     \\SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n");
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n");
}

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

    at.touch("f");
    at.write("CHECKSUM", "\
    SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\
    BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\
    BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\
    SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n");
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_does_not_contain("line is improperly formatted");
    scene
        .ucmd()
        .arg("--check")
        .arg("--strict")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_does_not_contain("line is improperly formatted");
    // inject invalid content
    at.append("CHECKSUM", "incorrect data");
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_contains("line is improperly formatted");
    scene
        .ucmd()
        .arg("--check")
        .arg("--strict")
        .arg("CHECKSUM")
        .fails()
        .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
        .stderr_contains("line is improperly formatted");
}

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

    at.touch("f");
    at.write(
        "CHECKSUM",
        "Sha1 (f) = da39a3ee5e6b4b0d3255bfef95601890afd80709\n",
    );
    scene.ucmd().arg("--check").arg("CHECKSUM").fails();
}

#[test]
fn test_cksum_check_invalid() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    let commands = [vec!["-a", "sha384"]];
    at.touch("f");
    at.touch("CHECKSUM");
    for command in &commands {
        let result = scene.ucmd().args(command).arg("f").succeeds();
        at.append("CHECKSUM", result.stdout_str());
    }
    // inject invalid content
    at.append("CHECKSUM", "again incorrect data\naze\n");
    scene
        .ucmd()
        .arg("--check")
        .arg("--strict")
        .arg("CHECKSUM")
        .fails()
        .stdout_contains("f: OK\n")
        .stderr_contains("2 lines");

    // without strict, it passes
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .succeeds()
        .stdout_contains("f: OK\n")
        .stderr_contains("2 lines");
}

#[test]
fn test_cksum_check_failed() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    let commands = [vec!["-a", "sha384"]];
    at.touch("f");
    at.touch("CHECKSUM");
    for command in &commands {
        let result = scene.ucmd().args(command).arg("f").succeeds();
        at.append("CHECKSUM", result.stdout_str());
    }
    // inject invalid content
    at.append("CHECKSUM", "again incorrect data\naze\nSM3 (input) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");

    let result = scene
        .ucmd()
        .arg("--check")
        .arg("--strict")
        .arg("CHECKSUM")
        .fails();

    assert!(
        result
            .stderr_str()
            .contains("input: No such file or directory")
    );
    assert!(
        result
            .stderr_str()
            .contains("2 lines are improperly formatted\n")
    );
    assert!(
        result
            .stderr_str()
            .contains("1 listed file could not be read\n")
    );
    assert!(result.stdout_str().contains("f: OK\n"));

    // without strict
    let result = scene.ucmd().arg("--check").arg("CHECKSUM").fails();

    assert!(
        result
            .stderr_str()
            .contains("input: No such file or directory")
    );
    assert!(
        result
            .stderr_str()
            .contains("2 lines are improperly formatted\n")
    );
    assert!(
        result
            .stderr_str()
            .contains("1 listed file could not be read\n")
    );
    assert!(result.stdout_str().contains("f: OK\n"));

    // tests with two files
    at.touch("CHECKSUM2");
    at.write("f2", "42");
    for command in &commands {
        let result = scene.ucmd().args(command).arg("f2").succeeds();
        at.append("CHECKSUM2", result.stdout_str());
    }
    // inject invalid content
    at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");
    at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");

    let result = scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .arg("CHECKSUM2")
        .fails();
    println!("result.stderr_str() {}", result.stderr_str());
    println!("result.stdout_str() {}", result.stdout_str());
    assert!(
        result
            .stderr_str()
            .contains("input2: No such file or directory")
    );
    assert!(
        result
            .stderr_str()
            .contains("4 lines are improperly formatted\n")
    );
    assert!(
        result
            .stderr_str()
            .contains("2 listed files could not be read\n")
    );
    assert!(result.stdout_str().contains("f: OK\n"));
    assert!(result.stdout_str().contains("2: OK\n"));
}

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

    let at = &scene.fixtures;
    at.touch("empty");
    at.write("f", "d41d8cd98f00b204e9800998ecf8427e *empty\n");

    scene
        .ucmd()
        .arg("-a")
        .arg("md5")
        .arg("--check")
        .arg("f")
        .succeeds()
        .stdout_contains("empty: OK");

    // with a second file
    at.write("not-empty", "42");
    at.write("f2", "a1d0c6e83f027327d8461063f4ac58a6 *not-empty\n");

    scene
        .ucmd()
        .arg("-a")
        .arg("md5")
        .arg("--check")
        .arg("f")
        .arg("f2")
        .succeeds()
        .stdout_contains("empty: OK")
        .stdout_contains("not-empty: OK");
}

// Manage the mixed behavior
// cksum --check -a sm3 CHECKSUMS
// when CHECKSUM contains among other lines:
// SHA384 (input) = f392fd0ae43879ced890c665a1d47179116b5eddf6fb5b49f4982746418afdcbd54ba5eedcd422af3592f57f666da285
#[test]
fn test_cksum_mixed() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    let commands = [
        vec!["-a", "sha384"],
        vec!["-a", "blake2b"],
        vec!["-a", "blake2b", "-l", "384"],
        vec!["-a", "sm3"],
    ];
    at.touch("f");
    at.touch("CHECKSUM");
    for command in &commands {
        let result = scene.ucmd().args(command).arg("f").succeeds();
        at.append("CHECKSUM", result.stdout_str());
    }
    println!("Content of CHECKSUM:\n{}", at.read("CHECKSUM"));
    let result = scene
        .ucmd()
        .arg("--check")
        .arg("-a")
        .arg("sm3")
        .arg("CHECKSUM")
        .succeeds();

    println!("result.stderr_str() {}", result.stderr_str());
    println!("result.stdout_str() {}", result.stdout_str());
    assert!(result.stdout_str().contains("f: OK"));
    assert!(
        result
            .stderr_str()
            .contains("3 lines are improperly formatted")
    );
}

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

    // Incorrect data at the start
    at.write(
        "check-file",
        "garbage MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28",
    );
    scene
        .ucmd()
        .arg("--check")
        .arg("check-file")
        .fails()
        .stderr_contains("check-file: no properly formatted checksum lines found");

    // Incorrect data at the end
    at.write(
        "check-file",
        "MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28 garbage",
    );
    scene
        .ucmd()
        .arg("--check")
        .arg("check-file")
        .fails()
        .stderr_contains("check-file: no properly formatted checksum lines found");
}

#[test]
fn test_md5_bits() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write(
        "f",
        "MD5-65536 (README.md) = e5773576fc75ff0f8eba14f61587ae28\n",
    );

    ucmd.arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
}

#[test]
fn test_blake2b_bits() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write(
        "f",
        "BLAKE2b-257 (README.md) = f9a984b70cf9a7549920864860fd1131c9fb6c0552def0b6dcce1d87b4ec4c5d\n"
    );

    ucmd.arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
}

#[test]
fn test_bsd_case() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    at.write("f", "bSD (README.md) = 0000\n");

    scene
        .ucmd()
        .arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
    at.write("f", "BsD (README.md) = 0000\n");

    scene
        .ucmd()
        .arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
}

#[test]
fn test_blake2d_tested_with_sha1() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write(
        "f",
        "BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n"
    );

    ucmd.arg("-a")
        .arg("sha1")
        .arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
}

#[test]
fn test_unknown_sha() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write("f", "SHA4 (README.md) = 00000000\n");

    ucmd.arg("-c")
        .arg("f")
        .fails()
        .stderr_contains("f: no properly formatted checksum lines found");
}

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

    at.mkdir("d");
    at.write(
        "f",
        "BLAKE2b (d) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n"
    );
    #[cfg(not(windows))]
    let err_msg = "cksum: d: Is a directory\n";
    #[cfg(windows)]
    let err_msg = "cksum: d: Permission denied\n";
    ucmd.arg("--check")
        .arg(at.subdir.join("f"))
        .fails()
        .stderr_contains(err_msg);
}

#[test]
fn test_check_base64_hashes() {
    let hashes = "MD5 (empty) = 1B2M2Y8AsgTpgAmY7PhCfg==\nSHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\nBLAKE2b (empty) = eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==\n";

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.touch("empty");
    at.write("check", hashes);

    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("check"))
        .succeeds()
        .stdout_is("empty: OK\nempty: OK\nempty: OK\n");
}

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

    // don't exist
    scene
        .ucmd()
        .arg("--check")
        .arg("empty")
        .arg("incorrect")
        .fails()
        .stderr_contains("empty: No such file ")
        .stderr_contains("incorrect: No such file ");

    at.touch("empty");
    at.touch("incorrect");

    // exists but incorrect
    scene
        .ucmd()
        .arg("--check")
        .arg("empty")
        .arg("incorrect")
        .fails()
        .stderr_contains("empty: no properly ")
        .stderr_contains("incorrect: no properly ");
}

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

    scene
        .ucmd()
        .arg("--check")
        .arg("missing")
        .fails()
        .stderr_only("cksum: missing: No such file or directory\n");
}

#[test]
fn test_check_comment_line() {
    // A comment in a checksum file shall be discarded unnoticed.

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo", "foo-content\n");
    at.write(
        "CHECKSUM-sha1",
        "\
        # This is a comment\n\
        SHA1 (foo) = 058ab38dd3603703b3a7063cf95dc51a4286b6fe\n\
        # next comment is empty\n#",
    );

    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM-sha1")
        .succeeds()
        .stdout_contains("foo: OK")
        .no_stderr();
}

#[test]
fn test_check_comment_only() {
    // A file only filled with comments is equivalent to an empty file,
    // and therefore produces an error.

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("CHECKSUM", "# This is a comment\n");

    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .fails()
        .stderr_contains("no properly formatted checksum lines found");
}

#[test]
fn test_check_comment_leading_space() {
    // A comment must have its '#' in first position on the line.
    // A space before it will raise a warning for improperly formatted line.

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo", "foo-content\n");
    at.write(
        "CHECKSUM-sha1",
        " # This is a comment\n\
        SHA1 (foo) = 058ab38dd3603703b3a7063cf95dc51a4286b6fe\n",
    );

    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM-sha1")
        .succeeds()
        .stdout_contains("foo: OK")
        .stderr_contains("WARNING: 1 line is improperly formatted");
}

/// This test checks alignment with GNU's error handling.
/// Windows has a different logic and is guarded by [`test_check_directory_error`].
#[cfg(not(windows))]
#[test]
fn test_check_failed_to_read() {
    // check `cksum`'s behavior when encountering directories or non existing files

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write(
        "CHECKSUM",
        "SHA1 (dir) = ffffffffffffffffffffffffffffffffffffffff\n\
        SHA1 (not-file) = ffffffffffffffffffffffffffffffffffffffff\n",
    );
    at.mkdir("dir");

    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .fails()
        .stdout_is(
            "dir: FAILED open or read\n\
            not-file: FAILED open or read\n",
        )
        .stderr_contains("cksum: WARNING: 2 listed files could not be read");

    // check with `--ignore-missing`
    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .arg("--ignore-missing")
        .fails()
        .stdout_is("dir: FAILED open or read\n")
        .stderr_contains("cksum: WARNING: 1 listed file could not be read");
}

#[test]
fn test_zero_multiple_file() {
    new_ucmd!()
        .arg("-z")
        .arg("alice_in_wonderland.txt")
        .arg("lorem_ipsum.txt")
        .succeeds()
        .stdout_is_fixture("zero_multiple_file.expected");
}

#[test]
fn test_zero_single_file() {
    new_ucmd!()
        .arg("--zero")
        .arg("alice_in_wonderland.txt")
        .succeeds()
        .stdout_is_fixture("zero_single_file.expected");
}

#[test]
fn test_check_trailing_space_fails() {
    // If a checksum line has trailing spaces after the digest,
    // it shall be considered improperly formatted.

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo", "foo-content\n");
    at.write(
        "CHECKSUM",
        "SHA1 (foo) = 058ab38dd3603703b3a7063cf95dc51a4286b6fe    \n",
    );

    scene
        .ucmd()
        .arg("--check")
        .arg("CHECKSUM")
        .fails()
        .no_stdout()
        .stderr_contains("CHECKSUM: no properly formatted checksum lines found");
}

/// Regroup tests related to the handling of non-utf-8 content
/// in checksum files.
/// These tests are excluded from Windows because it does not provide any safe
/// conversion between `OsString` and byte sequences for non-utf-8 strings.
mod check_encoding {

    // This test should pass on linux and macos.
    #[cfg(not(windows))]
    #[test]
    fn test_check_non_utf8_comment() {
        use super::*;
        let hashes =
        b"MD5 (empty) = 1B2M2Y8AsgTpgAmY7PhCfg==\n\
        # Comment with a non utf8 char: >>\xff<<\n\
        SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\n\
        BLAKE2b (empty) = eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==\n"
    ;

        let (at, mut cmd) = at_and_ucmd!();

        at.touch("empty");
        at.write_bytes("check", hashes);

        cmd.arg("--check")
            .arg(at.subdir.join("check"))
            .succeeds()
            .stdout_is("empty: OK\nempty: OK\nempty: OK\n")
            .no_stderr();
    }

    // This test should pass on linux. Windows and macos will fail to
    // create a file which name contains '\xff'.
    #[cfg(target_os = "linux")]
    #[test]
    fn test_check_non_utf8_filename() {
        use super::*;
        use std::{ffi::OsString, os::unix::ffi::OsStringExt};

        let scene = TestScenario::new(util_name!());
        let at = &scene.fixtures;
        let filename: OsString = OsStringExt::from_vec(b"funky\xffname".to_vec());
        at.touch(filename);

        // Checksum match
        at.write_bytes("check",
            b"SHA256 (funky\xffname) = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n");
        scene
            .ucmd()
            .arg("--check")
            .arg(at.subdir.join("check"))
            .succeeds()
            .stdout_is_bytes(b"'funky'$'\\377''name': OK\n")
            .no_stderr();

        // Checksum mismatch
        at.write_bytes("check",
            b"SHA256 (funky\xffname) = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n");
        scene
            .ucmd()
            .arg("--check")
            .arg(at.subdir.join("check"))
            .fails()
            .stdout_is_bytes(b"'funky'$'\\377''name': FAILED\n")
            .stderr_contains("1 computed checksum did NOT match");

        // file not found
        at.write_bytes("check",
            b"SHA256 (flakey\xffname) = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n");
        scene
            .ucmd()
            .arg("--check")
            .arg(at.subdir.join("check"))
            .fails()
            .stdout_is_bytes(b"'flakey'$'\\377''name': FAILED open or read\n")
            .stderr_contains("1 listed file could not be read");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_quoting_in_stderr() {
        use super::*;
        use std::{ffi::OsStr, os::unix::ffi::OsStrExt};

        let (at, mut cmd) = at_and_ucmd!();

        at.mkdir(<OsStr as OsStrExt>::from_bytes(b"FFF\xffDIR"));
        at.write_bytes(
            "check",
            b"SHA256 (FFF\xffFFF) = 29953405eaa3dcc41c37d1621d55b6a47eee93e05613e439e73295029740b10c\nSHA256 (FFF\xffDIR) = 29953405eaa3dcc41c37d1621d55b6a47eee93e05613e439e73295029740b10c\n",
        );

        cmd.arg("-c")
            .arg("check")
            .fails_with_code(1)
            .stdout_contains_bytes(b"'FFF'$'\\377''FFF': FAILED open or read")
            .stdout_contains_bytes(b"'FFF'$'\\377''DIR': FAILED open or read")
            .stderr_contains("'FFF'$'\\377''FFF': No such file or directory")
            .stderr_contains("'FFF'$'\\377''DIR': Is a directory");
    }
}

#[test]
fn test_check_blake_length_guess() {
    let correct_lines = [
        // Correct: The length is not explicit, but the checksum's size
        // matches the default parameter.
        "BLAKE2b (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d",
        // Correct: The length is explicitly given, and the checksum's size
        // matches the length.
        "BLAKE2b-512 (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d",
        // Correct: the checksum size is not default but
        // the length is explicitly given.
        "BLAKE2b-48 (foo.dat) = 171cdfdf84ed",
    ];
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo.dat", "foo");

    for line in correct_lines {
        at.write("foo.sums", line);
        scene
            .ucmd()
            .arg("--check")
            .arg(at.subdir.join("foo.sums"))
            .succeeds()
            .stdout_is("foo.dat: OK\n");
    }

    // Incorrect lines

    // This is incorrect because the algorithm provides no length,
    // and the checksum length is not default.
    let incorrect = "BLAKE2b (foo.dat) = 171cdfdf84ed";
    at.write("foo.sums", incorrect);
    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("foo.sums"))
        .fails()
        .stderr_contains("foo.sums: no properly formatted checksum lines found");

    // This is incorrect because the length hint provided doesn't match the
    // checksum length.
    let length_mismatch = "BLAKE2b-8 (foo.dat) = 171cdfdf84ed";
    at.write("foo.sums", length_mismatch);
    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("foo.sums"))
        .fails()
        .stderr_contains("foo.sums: no properly formatted checksum lines found");

    // In this case, validation should fail because even though the checksum
    // length matches the hint, it is above BLAKE2b's max (512).
    let too_long = "BLAKE2b-520 (/dev/null) = 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
    at.write("foo.sums", too_long);
    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("foo.sums"))
        .fails()
        .stderr_contains("foo.sums: no properly formatted checksum lines found");
}

#[test]
fn test_check_confusing_base64() {
    let cksum = "BLAKE2b-48 (foo.dat) = fc1f97C4";

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo.dat", "esq");
    at.write("foo.sums", cksum);

    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("foo.sums"))
        .succeeds()
        .stdout_is("foo.dat: OK\n");
}

/// This test checks that when a file contains several checksum lines
/// with different encoding, the decoding still works.
#[test]
fn test_check_mix_hex_base64() {
    let b64 = "BLAKE2b-128 (foo1.dat) = BBNuJPhdRwRlw9tm5Y7VbA==";
    let hex = "BLAKE2b-128 (foo2.dat) = 04136e24f85d470465c3db66e58ed56c";

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("foo1.dat", "foo");
    at.write("foo2.dat", "foo");

    at.write("hex_b64", &format!("{hex}\n{b64}"));
    at.write("b64_hex", &format!("{b64}\n{hex}"));

    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("hex_b64"))
        .succeeds()
        .stdout_only("foo2.dat: OK\nfoo1.dat: OK\n");

    scene
        .ucmd()
        .arg("--check")
        .arg(at.subdir.join("b64_hex"))
        .succeeds()
        .stdout_only("foo1.dat: OK\nfoo2.dat: OK\n");
}

/// This test ensures that an improperly formatted base64 checksum in a file
/// does not interrupt the processing of next lines.
#[test]
fn test_check_incorrectly_formatted_checksum_keeps_processing_b64() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    at.touch("f");

    let good_ck = "MD5 (f) = 1B2M2Y8AsgTpgAmY7PhCfg=="; // OK
    let bad_ck = "MD5 (f) = 1B2M2Y8AsgTpgAmY7PhCfg="; // Missing last '='

    // Good then Bad
    scene
        .ucmd()
        .arg("--check")
        .pipe_in([good_ck, bad_ck].join("\n").as_bytes().to_vec())
        .succeeds()
        .stdout_contains("f: OK")
        .stderr_contains("cksum: WARNING: 1 line is improperly formatted");

    // Bad then Good
    scene
        .ucmd()
        .arg("--check")
        .pipe_in([bad_ck, good_ck].join("\n").as_bytes().to_vec())
        .succeeds()
        .stdout_contains("f: OK")
        .stderr_contains("cksum: WARNING: 1 line is improperly formatted");
}

/// This test ensures that an improperly formatted hexadecimal checksum in a
/// file does not interrupt the processing of next lines.
#[test]
fn test_check_incorrectly_formatted_checksum_keeps_processing_hex() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;
    at.touch("f");

    let good_ck = "MD5 (f) = d41d8cd98f00b204e9800998ecf8427e"; // OK
    let bad_ck = "MD5 (f) = d41d8cd98f00b204e9800998ecf8427"; // Missing last

    // Good then Bad
    scene
        .ucmd()
        .arg("--check")
        .pipe_in([good_ck, bad_ck].join("\n").as_bytes().to_vec())
        .succeeds()
        .stdout_contains("f: OK")
        .stderr_contains("cksum: WARNING: 1 line is improperly formatted");

    // Bad then Good
    scene
        .ucmd()
        .arg("--check")
        .pipe_in([bad_ck, good_ck].join("\n").as_bytes().to_vec())
        .succeeds()
        .stdout_contains("f: OK")
        .stderr_contains("cksum: WARNING: 1 line is improperly formatted");
}

/// This module reimplements the cksum-base64.pl GNU test.
mod gnu_cksum_base64 {
    use super::*;
    use uutests::util::log_info;

    const PAIRS: [(&str, &str); 12] = [
        ("sysv", "0 0 f"),
        ("bsd", "00000     0 f"),
        ("crc", "4294967295 0 f"),
        ("crc32b", "0 0 f"),
        ("md5", "1B2M2Y8AsgTpgAmY7PhCfg=="),
        ("sha1", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="),
        ("sha224", "0UoCjCo6K8lHYQK7KII0xBWisB+CjqYqxbPkLw=="),
        ("sha256", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="),
        (
            "sha384",
            "OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
        ),
        (
            "sha512",
            "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==",
        ),
        (
            "blake2b",
            "eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==",
        ),
        ("sm3", "GrIdg1XPoX+OYRlIMegajyK+yMco/vt0ftA161CCqis="),
    ];

    fn make_scene() -> TestScenario {
        let scene = TestScenario::new(util_name!());
        let at = &scene.fixtures;
        at.touch("f");

        scene
    }

    fn output_format(algo: &str, digest: &str) -> String {
        if ["sysv", "bsd", "crc", "crc32b"].contains(&algo) {
            digest.to_string()
        } else {
            format!("{} (f) = {digest}", algo.to_uppercase()).replace("BLAKE2B", "BLAKE2b")
        }
    }

    #[test]
    fn test_generating() {
        // Ensure that each algorithm works with `--base64`.
        let scene = make_scene();

        for (algo, digest) in PAIRS {
            log_info("ALGORITHM", algo);
            scene
                .ucmd()
                .arg("--base64")
                .arg("-a")
                .arg(algo)
                .arg("f")
                .succeeds()
                .stdout_only(format!("{}\n", output_format(algo, digest)));
        }
    }

    #[test]
    fn test_chk() {
        // For each algorithm that accepts `--check`,
        // ensure that it works with base64 digests.
        let scene = make_scene();

        for (algo, digest) in PAIRS {
            if ["sysv", "bsd", "crc", "crc32b"].contains(&algo) {
                // These algorithms do not accept `--check`
                scene
                    .ucmd()
                    .arg("--check")
                    .arg("-a")
                    .arg(algo)
                    .fails()
                    .stderr_only(
                        "cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}\n",
                    );
                continue;
            }

            let line = output_format(algo, digest);
            scene
                .ucmd()
                .arg("--check")
                .arg("--strict")
                .pipe_in(line)
                .succeeds()
                .stdout_only("f: OK\n");
        }
    }

    #[test]
    fn test_chk_eq1() {
        // For digests ending with '=', ensure `--check` fails if '=' is removed.
        let scene = make_scene();

        for (algo, digest) in PAIRS {
            if !digest.ends_with('=') {
                continue;
            }

            let mut line = output_format(algo, digest);
            if line.ends_with('=') {
                line.pop();
            }

            log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), "");
            scene
                .ucmd()
                .arg("--check")
                .pipe_in(line)
                .fails()
                .no_stdout()
                .stderr_contains("no properly formatted checksum lines found");
        }
    }

    #[test]
    fn test_chk_eq2() {
        // For digests ending with '==',
        // ensure `--check` fails if '==' is removed.
        let scene = make_scene();

        for (algo, digest) in PAIRS {
            if !digest.ends_with("==") {
                continue;
            }

            let line = output_format(algo, digest);
            let line = line.trim_end_matches("==");

            log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), "");
            scene
                .ucmd()
                .arg("--check")
                .pipe_in(line)
                .fails()
                .no_stdout()
                .stderr_contains("no properly formatted checksum lines found");
        }
    }
}

/// This module reimplements the cksum-base64-untagged.sh GNU test.
mod gnu_cksum_base64_untagged {
    use super::*;

    macro_rules! decl_sha_test {
        ($id:ident, $algo:literal, $len:expr) => {
            mod $id {
                use super::*;

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

                    at.write("inp", "test input\n");

                    let compute = ts
                        .ucmd()
                        .arg("-a")
                        .arg($algo)
                        .arg("-l")
                        .arg(stringify!($len))
                        .arg("--base64")
                        .arg("--untagged")
                        .arg("inp")
                        .succeeds();

                    at.write_bytes("check", compute.stdout());

                    ts.ucmd()
                        .arg("-a")
                        .arg($algo)
                        .arg("--check")
                        .arg("check")
                        .succeeds()
                        .stdout_only("inp: OK\n");

                    at.write("check", "  inp");

                    ts.ucmd()
                        .arg("-a")
                        .arg($algo)
                        .arg("check")
                        .fails()
                        .stderr_contains(concat!(
                            "--algorithm=",
                            $algo,
                            " requires specifying --length"
                        ));
                }
            }
        };
    }

    decl_sha_test!(sha2_224, "sha2", 224);
    decl_sha_test!(sha2_256, "sha2", 256);
    decl_sha_test!(sha2_384, "sha2", 384);
    decl_sha_test!(sha2_512, "sha2", 512);
    decl_sha_test!(sha3_224, "sha3", 224);
    decl_sha_test!(sha3_256, "sha3", 256);
    decl_sha_test!(sha3_384, "sha3", 384);
    decl_sha_test!(sha3_512, "sha3", 512);

    macro_rules! decl_blake_test {
        ($id:ident, $len:expr) => {
            mod $id {
                use super::*;

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

                    at.write("inp", "test input\n");

                    let compute = ts
                        .ucmd()
                        .arg("-a")
                        .arg("blake2b")
                        .arg("-l")
                        .arg(stringify!($len))
                        .arg("--base64")
                        .arg("--untagged")
                        .arg("inp")
                        .succeeds();

                    at.write_bytes("check", compute.stdout());

                    ts.ucmd()
                        .arg("-a")
                        .arg("blake2b")
                        .arg("--check")
                        .arg("check")
                        .succeeds()
                        .stdout_only("inp: OK\n");
                }
            }
        };
    }

    decl_blake_test!(blake2b_8, 8);
    decl_blake_test!(blake2b_216, 216);
    decl_blake_test!(blake2b_224, 224);
    decl_blake_test!(blake2b_232, 232);
    decl_blake_test!(blake2b_248, 248);
    decl_blake_test!(blake2b_256, 256);
    decl_blake_test!(blake2b_264, 264);
    decl_blake_test!(blake2b_376, 376);
    decl_blake_test!(blake2b_384, 384);
    decl_blake_test!(blake2b_392, 392);
    decl_blake_test!(blake2b_504, 504);
    decl_blake_test!(blake2b_512, 512);
}
/// This module reimplements the cksum-c.sh GNU test.
mod gnu_cksum_c {
    use super::*;

    const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e";

    fn make_scene() -> TestScenario {
        let scene = TestScenario::new(util_name!());
        let at = &scene.fixtures;
        at.write("input", "9\n7\n1\n4\n2\n6\n3\n5\n8\n10\n");

        let algos: &[&[&str]] = &[
            &["-a", "sha384"],
            &["-a", "blake2b"],
            &["-a", "blake2b", "-l", "384"],
            &["-a", "sm3"],
        ];

        for args in algos {
            let result = scene.ucmd().args(args).succeeds();
            let stdout = result.stdout();
            at.append_bytes("CHECKSUMS", stdout);
        }

        scene
    }

    fn make_scene_with_comment() -> TestScenario {
        let scene = make_scene();

        scene
            .fixtures
            .append("CHECKSUMS", "# Very important comment\n");

        scene
    }

    fn make_scene_with_invalid_line() -> TestScenario {
        let scene = make_scene_with_comment();

        scene.fixtures.append("CHECKSUMS", "invalid_line\n");

        scene
    }

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

        at.write(
            "sha2-bad-length.sum",
            "SHA2-128 (/dev/null) = 38b060a751ac96384cd9327eb1b1e36a",
        );

        ucmd.arg("--check")
            .arg("sha2-bad-length.sum")
            .fails()
            .stderr_contains("sha2-bad-length.sum: no properly formatted checksum lines found");
    }

    #[test]
    #[cfg_attr(not(unix), ignore = "/dev/null is only available on UNIX")]
    fn test_untagged_base64_matching_tag() {
        let (at, mut ucmd) = at_and_ucmd!();

        at.write("tag-prefix.sum", "SHA1+++++++++++++++++++++++=  /dev/null");

        ucmd.arg("--check")
            .arg("-a")
            .arg("sha1")
            .arg("tag-prefix.sum")
            .fails()
            .stderr_contains("WARNING: 1 computed checksum did NOT match");
    }

    #[test]
    #[cfg_attr(windows, ignore = "Awkward filename is not supported on windows")]
    fn test_awkward_filename() {
        let ts = TestScenario::new(util_name!());
        let at = &ts.fixtures;

        let awkward_file = "abc (f) = abc";

        at.touch(awkward_file);

        let result = ts.ucmd().arg("-a").arg("sha1").arg(awkward_file).succeeds();

        at.write_bytes("tag-awkward.sum", result.stdout());

        ts.ucmd().arg("-c").arg("tag-awkward.sum").succeeds();
    }

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

        let filename = "test_file";
        at.touch(filename);

        let checksum_result = ts.ucmd().arg("-a").arg("sha256").arg(filename).succeeds();
        let valid_checksum_line = checksum_result.stdout_str().trim();

        let signed_content = format!(
            "-----BEGIN PGP SIGNED MESSAGE-----\n\
             Hash: SHA256\n\
             \n\
             # This is a comment that should be ignored\n\
             {valid_checksum_line}\n\
             -----BEGIN PGP SIGNATURE-----\n\
             \n\
             xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\
             xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\
             =PlaceHolder\n\
             -----END PGP SIGNATURE-----"
        );

        at.write("signed_CHECKSUMS", &signed_content);

        ts.ucmd().arg("--check").arg("signed_CHECKSUMS").succeeds();
    }

    #[test]
    fn test_check_individual_digests_in_mixed_file() {
        let scene = make_scene();

        scene
            .ucmd()
            .arg("--check")
            .arg("-a")
            .arg("sm3")
            .arg("CHECKSUMS")
            .succeeds();
    }

    #[test]
    fn test_check_against_older_non_hex_formats() {
        let scene = make_scene();

        scene
            .ucmd()
            .arg("-c")
            .arg("-a")
            .arg("crc")
            .arg("CHECKSUMS")
            .fails();

        let crc_cmd = scene.ucmd().arg("-a").arg("crc").arg("input").succeeds();
        let crc_cmd_out = crc_cmd.stdout();
        scene.fixtures.write_bytes("CHECKSUMS.crc", crc_cmd_out);

        scene.ucmd().arg("-c").arg("CHECKSUMS.crc").fails();
    }

    #[test]
    fn test_status() {
        let scene = make_scene();

        scene
            .ucmd()
            .arg("--status")
            .arg("--check")
            .arg("CHECKSUMS")
            .succeeds()
            .no_output();
    }

    #[test]
    fn test_status_with_comment() {
        let scene = make_scene_with_comment();

        scene
            .ucmd()
            .arg("--status")
            .arg("--check")
            .arg("CHECKSUMS")
            .succeeds()
            .no_output();
    }

    #[test]
    fn test_check_strict() {
        let scene = make_scene_with_invalid_line();

        // without strict, succeeds
        scene
            .ucmd()
            .arg("--check")
            .arg("CHECKSUMS")
            .succeeds()
            .stderr_contains("1 line is improperly formatted");

        // with strict, fails
        scene
            .ucmd()
            .arg("--strict")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stderr_contains("1 line is improperly formatted");
    }

    fn make_scene_with_two_invalid_lines() -> TestScenario {
        let scene = make_scene_with_comment();

        scene
            .fixtures
            .append("CHECKSUMS", "invalid_line\ninvalid_line\n");

        scene
    }

    #[test]
    fn test_check_strict_plural_checks() {
        let scene = make_scene_with_two_invalid_lines();

        scene
            .ucmd()
            .arg("--strict")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stderr_contains("2 lines are improperly formatted");
    }

    fn make_scene_with_incorrect_checksum() -> TestScenario {
        let scene = make_scene_with_two_invalid_lines();

        scene
            .fixtures
            .append("CHECKSUMS", &format!("SM3 (input) = {INVALID_SUM}\n"));

        scene
    }

    #[test]
    fn test_check_with_incorrect_checksum() {
        let scene = make_scene_with_incorrect_checksum();

        scene
            .ucmd()
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stdout_contains("input: FAILED")
            .stderr_contains("1 computed checksum did NOT match");

        // also fails with strict
        scene
            .ucmd()
            .arg("--strict")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stdout_contains("input: FAILED")
            .stderr_contains("1 computed checksum did NOT match");
    }

    #[test]
    fn test_status_with_errors() {
        let scene = make_scene_with_incorrect_checksum();

        scene
            .ucmd()
            .arg("--status")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .no_output();
    }

    #[test]
    fn test_check_with_non_existing_file() {
        let scene = make_scene();
        scene
            .fixtures
            .write("CHECKSUMS2", &format!("SM3 (input2) = {INVALID_SUM}\n"));

        scene
            .ucmd()
            .arg("--check")
            .arg("CHECKSUMS2")
            .fails()
            .stdout_contains("input2: FAILED open or read")
            .stderr_contains("1 listed file could not be read");

        // also fails with strict
        scene
            .ucmd()
            .arg("--strict")
            .arg("--check")
            .arg("CHECKSUMS2")
            .fails()
            .stdout_contains("input2: FAILED open or read")
            .stderr_contains("1 listed file could not be read");
    }

    fn make_scene_with_another_improperly_formatted() -> TestScenario {
        let scene = make_scene_with_incorrect_checksum();

        scene.fixtures.append(
            "CHECKSUMS",
            &format!("BLAKE2b (missing-file) = {INVALID_SUM}\n"),
        );

        scene
    }

    #[test]
    fn test_warn() {
        let scene = make_scene_with_another_improperly_formatted();

        scene
            .ucmd()
            .arg("--warn")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stderr_contains("CHECKSUMS: 6: improperly formatted SM3 checksum line")
            .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line");
    }

    fn make_scene_with_checksum_missing() -> TestScenario {
        let scene = make_scene_with_another_improperly_formatted();

        scene.fixtures.write(
            "CHECKSUMS-missing",
            &format!("SM3 (nonexistent) = {INVALID_SUM}\n"),
        );

        scene
    }

    #[test]
    fn test_ignore_missing() {
        let scene = make_scene_with_checksum_missing();

        scene
            .ucmd()
            .arg("--ignore-missing")
            .arg("--check")
            .arg("CHECKSUMS-missing")
            .fails()
            .stdout_does_not_contain("nonexistent: No such file or directory")
            .stdout_does_not_contain("nonexistent: FAILED open or read")
            .stderr_contains("CHECKSUMS-missing: no file was verified");
    }

    #[test]
    fn test_ignore_missing_stdin() {
        let scene = make_scene_with_checksum_missing();

        scene
            .ucmd()
            .arg("--ignore-missing")
            .arg("--check")
            .pipe_in_fixture("CHECKSUMS-missing")
            .fails()
            .no_stdout()
            .stderr_contains("'standard input': no file was verified");
    }

    #[test]
    fn test_status_and_warn() {
        let scene = make_scene_with_checksum_missing();

        // --status before --warn
        scene
            .ucmd()
            .arg("--status")
            .arg("--warn")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line")
            .stderr_contains("WARNING: 3 lines are improperly formatted")
            .stderr_contains("WARNING: 1 computed checksum did NOT match");

        // --warn before --status (status hides the results)
        scene
            .ucmd()
            .arg("--warn")
            .arg("--status")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .stderr_does_not_contain("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line")
            .stderr_does_not_contain("WARNING: 3 lines are improperly formatted")
            .stderr_does_not_contain("WARNING: 1 computed checksum did NOT match");
    }

    #[test]
    fn test_status_and_ignore_missing() {
        let scene = make_scene_with_checksum_missing();

        scene
            .ucmd()
            .arg("--status")
            .arg("--ignore-missing")
            .arg("--check")
            .arg("CHECKSUMS")
            .fails()
            .no_output();
    }

    #[test]
    fn test_status_warn_and_ignore_missing() {
        let scene = make_scene_with_checksum_missing();

        scene
            .ucmd()
            .arg("--status")
            .arg("--warn")
            .arg("--ignore-missing")
            .arg("--check")
            .arg("CHECKSUMS-missing")
            .fails()
            .stderr_contains("CHECKSUMS-missing: no file was verified")
            .stdout_does_not_contain("nonexistent: No such file or directory");
    }

    #[test]
    fn test_check_several_files_dont_exist() {
        let scene = make_scene();

        scene
            .ucmd()
            .arg("--check")
            .arg("non-existing-1")
            .arg("non-existing-2")
            .fails()
            .stderr_contains("non-existing-1: No such file or directory")
            .stderr_contains("non-existing-2: No such file or directory");
    }

    #[test]
    fn test_check_several_files_empty() {
        let scene = make_scene();
        scene.fixtures.touch("empty-1");
        scene.fixtures.touch("empty-2");

        scene
            .ucmd()
            .arg("--check")
            .arg("empty-1")
            .arg("empty-2")
            .fails()
            .stderr_contains("empty-1: no properly formatted checksum lines found")
            .stderr_contains("empty-2: no properly formatted checksum lines found");
    }
}

/// The tests in this module check the behavior of cksum when given different
/// checksum formats and algorithms in the same file, while specifying an
/// algorithm on CLI or not.
mod format_mix {
    use super::*;

    // First line is algo-based, second one is not
    const INPUT_ALGO_NON_ALGO: &str = "\
        BLAKE2b (bar) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\
        786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce  foo";

    // First line is non algo-based, second one is
    const INPUT_NON_ALGO_ALGO: &str = "\
        786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce  foo\n\
        BLAKE2b (bar) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce";

    /// Make a simple scene with foo and bar empty files
    fn make_scene() -> TestScenario {
        let scene = TestScenario::new(util_name!());
        let at = &scene.fixtures;

        at.touch("foo");
        at.touch("bar");

        scene
    }

    #[test]
    fn test_check_cli_algo_non_algo() {
        let scene = make_scene();
        scene
            .ucmd()
            .arg("--check")
            .arg("--algo=blake2b")
            .pipe_in(INPUT_ALGO_NON_ALGO)
            .succeeds()
            .stdout_contains("bar: OK\nfoo: OK")
            .no_stderr();
    }

    #[test]
    fn test_check_cli_non_algo_algo() {
        let scene = make_scene();
        scene
            .ucmd()
            .arg("--check")
            .arg("--algo=blake2b")
            .pipe_in(INPUT_NON_ALGO_ALGO)
            .succeeds()
            .stdout_contains("foo: OK\nbar: OK")
            .no_stderr();
    }

    #[test]
    fn test_check_algo_non_algo() {
        let scene = make_scene();
        scene
            .ucmd()
            .arg("--check")
            .pipe_in(INPUT_ALGO_NON_ALGO)
            .succeeds()
            .stdout_contains("bar: OK")
            .stderr_contains("cksum: WARNING: 1 line is improperly formatted");
    }

    #[test]
    fn test_check_non_algo_algo() {
        let scene = make_scene();
        scene
            .ucmd()
            .arg("--check")
            .pipe_in(INPUT_NON_ALGO_ALGO)
            .succeeds()
            .stdout_contains("bar: OK")
            .stderr_contains("cksum: WARNING: 1 line is improperly formatted");
    }
}

#[cfg(not(target_os = "android"))]
mod debug_flag {
    use super::*;

    #[test]
    fn test_debug_flag() {
        // Test with default CRC algorithm - should output CPU feature detection
        new_ucmd!()
            .arg("--debug")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stdout_is_fixture("crc_single_file.expected")
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with MD5 algorithm - CPU detection should be same regardless of algorithm
        new_ucmd!()
            .arg("--debug")
            .arg("-a")
            .arg("md5")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stdout_is_fixture("md5_single_file.expected")
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with stdin - CPU detection should appear once
        new_ucmd!()
            .arg("--debug")
            .pipe_in("test")
            .succeeds()
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with multiple files - CPU detection should appear once, not per file
        new_ucmd!()
            .arg("--debug")
            .arg("lorem_ipsum.txt")
            .arg("alice_in_wonderland.txt")
            .succeeds()
            .stdout_is_fixture("crc_multiple_files.expected")
            .stderr_str_check(|stderr| {
                // Verify CPU detection happens only once by checking the count of each feature line
                let avx512_count = stderr
                    .lines()
                    .filter(|line| line.contains("avx512"))
                    .count();
                let avx2_count = stderr.lines().filter(|line| line.contains("avx2")).count();
                let pclmul_count = stderr
                    .lines()
                    .filter(|line| line.contains("pclmul"))
                    .count();

                avx512_count == 1 && avx2_count == 1 && pclmul_count == 1
            });
    }

    #[test]
    fn test_debug_with_algorithms() {
        // Test with SHA256 - CPU detection should be same regardless of algorithm
        new_ucmd!()
            .arg("--debug")
            .arg("-a")
            .arg("sha256")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with BLAKE2b default length
        new_ucmd!()
            .arg("--debug")
            .arg("-a")
            .arg("blake2b")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with BLAKE2b custom length
        new_ucmd!()
            .arg("--debug")
            .arg("-a")
            .arg("blake2b")
            .arg("--length")
            .arg("256")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");

        // Test with SHA1
        new_ucmd!()
            .arg("--debug")
            .arg("-a")
            .arg("sha1")
            .arg("lorem_ipsum.txt")
            .succeeds()
            .stderr_contains("avx512")
            .stderr_contains("avx2")
            .stderr_contains("pclmul");
    }
}

#[test]
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn test_check_file_with_io_error() {
    // /proc/self/mem causes EIO when read without proper seeking
    new_ucmd!()
        .arg("-a")
        .arg("md5")
        .arg("--check")
        .pipe_in("d8e8fca2dc0f896fd7cb4cb0031ba249  /proc/self/mem\n")
        .fails()
        .stderr_contains("Input/output error")
        .stdout_contains("FAILED open or read");
}

#[test]
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn test_check_checkfile_with_io_error() {
    // /proc/self/mem causes EIO when read without proper seeking
    new_ucmd!()
        .arg("-a")
        .arg("md5")
        .arg("--check")
        .arg("/proc/self/mem")
        .fails()
        .stderr_contains("/proc/self/mem: read error")
        .no_stdout();
}

#[rstest]
#[case::default_length(
    &[],
    "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf"
)]
#[case::pass_default_length(
    &["-l", "256"],
    "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf"
)]
#[case::smaller_length(
    &["-l", "128"],
    "ac8549b2861a151896ab721bd29d7a20"
)]
#[case::bigger_length(
    &["-l", "264"],
    "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdfc2"
)]
#[case::length_0(
    &["-l", "0"],
    "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf"
)]
#[case::length_1(
    &["-l", "1"],
    "00"
)]
#[case::length_2(
    &["-l", "2"],
    "00"
)]
#[case::length_3(
    &["-l", "3"],
    "04"
)]
#[case::length_4(
    &["-l", "4"],
    "0c"
)]
#[case::length_5(
    &["-l", "5"],
    "0c"
)]
#[case::length_6(
    &["-l", "6"],
    "2c"
)]
#[case::length_7(
    &["-l", "7"],
    "2c"
)]
#[case::length_8(
    &["-l", "8"],
    "ac"
)]
fn test_shake128(#[case] args: &[&str], #[case] expected: &str) {
    let bit_len = if args.is_empty() || args[1] == "0" {
        "256"
    } else {
        args[1]
    };

    new_ucmd!()
        .arg("-a")
        .arg("shake128")
        .args(args)
        .pipe_in("xxx")
        .succeeds()
        .stdout_only(format!("SHAKE128-{bit_len} (-) = {expected}\n"));
}

#[rstest]
#[case::default_length(
    &[],
    "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba"
)]
#[case::pass_default_length(
    &["-l", "512"],
    "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba"
)]
#[case::smaller_length(
    &["-l", "128"],
    "2fa631503c3ea5fe85131dbfa2480518"
)]
#[case::bigger_length(
    &["-l", "1024"],
    "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba18783d25f8b36d92b8607f016352b5c405945a7859a8339201728f680647324d1b8ea93a01d2ef965dadf4a1bee3ff044ed2b4bd95e4311f5e3f2cd5bae0b7c6"
)]
#[case::length_0(
    &["-l", "0"],
    "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba"
)]
#[case::length_1(
    &["-l", "1"],
    "01"
)]
#[case::length_2(
    &["-l", "2"],
    "03"
)]
#[case::length_3(
    &["-l", "3"],
    "07"
)]
#[case::length_4(
    &["-l", "4"],
    "0f"
)]
#[case::length_5(
    &["-l", "5"],
    "0f"
)]
#[case::length_6(
    &["-l", "6"],
    "2f"
)]
#[case::length_7(
    &["-l", "7"],
    "2f"
)]
#[case::length_8(
    &["-l", "8"],
    "2f"
)]
fn test_shake256(#[case] args: &[&str], #[case] expected: &str) {
    let bit_len = if args.is_empty() || args[1] == "0" {
        "512"
    } else {
        args[1]
    };

    new_ucmd!()
        .arg("-a")
        .arg("shake256")
        .args(args)
        .pipe_in("xxx")
        .succeeds()
        .stdout_only(format!("SHAKE256-{bit_len} (-) = {expected}\n"));
}

#[test]
fn test_check_shake128_no_length() {
    const INPUT_SHAKE128_CORRECT_LEN: &str =
        "SHAKE128 (bar) = ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf";
    const INPUT_SHAKE128_WRONG_LEN: &str = "SHAKE128 (bar) = ac8549b2861a151896ab721bd29d7a20";

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("bar", "xxx");

    scene
        .ucmd()
        .arg("-a")
        .arg("shake128")
        .arg("-c")
        .pipe_in(INPUT_SHAKE128_CORRECT_LEN)
        .succeeds();

    scene
        .ucmd()
        .arg("-a")
        .arg("shake128")
        .arg("-c")
        .pipe_in(INPUT_SHAKE128_WRONG_LEN)
        .fails()
        .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n");
}

#[test]
fn test_check_shake256_no_length() {
    const INPUT_SHAKE256_CORRECT_LEN: &str = "SHAKE256 (bar) = 2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba";
    const INPUT_SHAKE256_WRONG_LEN: &str =
        "SHAKE256 (bar) = 2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7";

    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    at.write("bar", "xxx");

    scene
        .ucmd()
        .arg("-c")
        .pipe_in(INPUT_SHAKE256_CORRECT_LEN)
        .succeeds();

    scene
        .ucmd()
        .arg("-c")
        .pipe_in(INPUT_SHAKE256_WRONG_LEN)
        .fails()
        .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n");
}

#[template]
#[rstest]
#[case::no_length(
    b"foo",
    "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9",
    None
)]
#[case(
    b"foo",
    "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9",
    Some(0)
)]
#[case(
    b"foo",
    "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9",
    Some(256)
)]
#[case(
    b"foo",
    "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c",
    Some(400)
)]
#[case(b"foo", "04e0bb39f3", Some(40))]
#[case(b"foo", "04e0", Some(16))]
#[case(b"foo", "04", Some(8))]
fn test_blake3(#[case] input: &[u8], #[case] expected: &str, #[case] length: Option<usize>) {}

#[apply(test_blake3)]
fn test_compute_blake3(
    #[case] input: &[u8],
    #[case] expected: &str,
    #[case] length: Option<usize>,
) {
    let length_args: &[String] = if let Some(len) = length {
        &["-l".into(), len.to_string()]
    } else {
        &[]
    };

    new_ucmd!()
        .arg("-a")
        .arg("blake3")
        .args(length_args)
        .pipe_in(input)
        .succeeds()
        .stdout_only(format!(
            "BLAKE3{} (-) = {expected}\n",
            match length {
                Some(0) | None => "-256".into(),
                Some(i) => format!("-{i}"),
            }
        ));

    // with --untagged
    new_ucmd!()
        .arg("-a")
        .arg("blake3")
        .arg("--untagged")
        .args(length_args)
        .pipe_in(input)
        .succeeds()
        .stdout_only(format!("{expected}  -\n"));
}

#[apply(test_blake3)]
fn test_check_blake3_tagged(
    #[case] input: &[u8],
    #[case] digest: &str,
    #[case] opt_len: Option<usize>,
) {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write_bytes("FILE", input);

    let len = match opt_len {
        Some(0) => "-256".into(),
        Some(i) => format!("-{i}"),
        None => String::new(),
    };

    let tagged = format!("BLAKE3{len} (FILE) = {digest}");

    ucmd.arg("-c")
        .arg("-a")
        .arg("blake3")
        .pipe_in(tagged)
        .succeeds()
        .stdout_only("FILE: OK\n");
}

#[apply(test_blake3)]
#[allow(clippy::used_underscore_binding)]
fn test_check_blake3_untagged(
    #[case] input: &[u8],
    #[case] digest: &str,
    #[case] _opt_len: Option<usize>,
) {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write_bytes("FILE", input);

    let untagged = format!("{digest} FILE");

    ucmd.arg("-c")
        .arg("-a")
        .arg("blake3")
        .pipe_in(untagged)
        .succeeds()
        .stdout_only("FILE: OK\n");
}
