// 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 bsdutils toybox
#[cfg(target_os = "linux")]
use std::os::unix::ffi::OsStringExt;
use uutests::at_and_ucmd;
use uutests::new_ucmd;

struct TestData<'b> {
    name: &'b str,
    args: &'b [&'b str],
    ins: &'b [&'b str],
    out: &'b str,
}

const EXAMPLE_DATA: &[TestData] = &[
    // Ensure that paste properly handles files lacking a final newline.
    TestData {
        name: "no-nl-1",
        args: &[],
        ins: &["a", "b"],
        out: "a\tb\n",
    },
    TestData {
        name: "no-nl-2",
        args: &[],
        ins: &["a\n", "b"],
        out: "a\tb\n",
    },
    TestData {
        name: "no-nl-3",
        args: &[],
        ins: &["a", "b\n"],
        out: "a\tb\n",
    },
    TestData {
        name: "no-nl-4",
        args: &[],
        ins: &["a\n", "b\n"],
        out: "a\tb\n",
    },
    TestData {
        name: "zno-nl-1",
        args: &["-z"],
        ins: &["a", "b"],
        out: "a\tb\0",
    },
    TestData {
        name: "zno-nl-2",
        args: &["-z"],
        ins: &["a\0", "b"],
        out: "a\tb\0",
    },
    TestData {
        name: "zno-nl-3",
        args: &["-z"],
        ins: &["a", "b\0"],
        out: "a\tb\0",
    },
    TestData {
        name: "zno-nl-4",
        args: &["-z"],
        ins: &["a\0", "b\0"],
        out: "a\tb\0",
    },
    // Same as above, but with a two lines in each input file and the
    // addition of the -d option to make SPACE be the output
    // delimiter.
    TestData {
        name: "no-nla-1",
        args: &["-d", " "],
        ins: &["1\na", "2\nb"],
        out: "1 2\na b\n",
    },
    TestData {
        name: "no-nla-2",
        args: &["-d", " "],
        ins: &["1\na\n", "2\nb"],
        out: "1 2\na b\n",
    },
    TestData {
        name: "no-nla-3",
        args: &["-d", " "],
        ins: &["1\na", "2\nb\n"],
        out: "1 2\na b\n",
    },
    TestData {
        name: "no-nla-4",
        args: &["-d", " "],
        ins: &["1\na\n", "2\nb\n"],
        out: "1 2\na b\n",
    },
    TestData {
        name: "zno-nla1",
        args: &["-zd", " "],
        ins: &["1\0a", "2\0b"],
        out: "1 2\0a b\0",
    },
    TestData {
        name: "zno-nla2",
        args: &["-zd", " "],
        ins: &["1\0a\0", "2\0b"],
        out: "1 2\0a b\0",
    },
    TestData {
        name: "zno-nla3",
        args: &["-zd", " "],
        ins: &["1\0a", "2\0b\0"],
        out: "1 2\0a b\0",
    },
    TestData {
        name: "zno-nla4",
        args: &["-zd", " "],
        ins: &["1\0a\0", "2\0b\0"],
        out: "1 2\0a b\0",
    },
    TestData {
        name: "multibyte-delim",
        args: &["-d", "💣"],
        ins: &["1\na\n", "2\nb\n"],
        out: "1💣2\na💣b\n",
    },
    TestData {
        name: "multibyte-delim-serial",
        args: &["-d", "💣", "-s"],
        ins: &["1\na\n", "2\nb\n"],
        out: "1💣a\n2💣b\n",
    },
    TestData {
        name: "trailing whitespace",
        args: &["-d", "|"],
        ins: &["1 \na \n", "2\t\nb\t\n"],
        out: "1 |2\t\na |b\t\n",
    },
    TestData {
        name: "utf8-2byte-delim",
        args: &["-d", "\u{00A2}"],
        ins: &["1\n2\n", "a\nb\n"],
        out: "1\u{00A2}a\n2\u{00A2}b\n",
    },
    TestData {
        name: "utf8-3byte-delim",
        args: &["-d", "\u{20AC}"],
        ins: &["1\n2\n", "a\nb\n"],
        out: "1\u{20AC}a\n2\u{20AC}b\n",
    },
    TestData {
        name: "utf8-4byte-delim",
        args: &["-d", "\u{1F600}", "-s"],
        ins: &["1\n2\n3\n"],
        out: "1\u{1F600}2\u{1F600}3\n",
    },
    TestData {
        name: "utf8-multi-delim-cycle",
        args: &["-d", "\u{00A2}\u{20AC}"],
        ins: &["a\nb\nc\n", "1\n2\n3\n", "x\ny\nz\n"],
        out: "a\u{00A2}1\u{20AC}x\nb\u{00A2}2\u{20AC}y\nc\u{00A2}3\u{20AC}z\n",
    },
];

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

#[test]
fn test_combine_pairs_of_lines() {
    for s in ["-s", "--serial"] {
        for d in ["-d", "--delimiters"] {
            new_ucmd!()
                .args(&[s, d, "\t\n", "html_colors.txt"])
                .succeeds()
                .stdout_is_fixture("html_colors.expected");
        }
    }
}

#[test]
fn test_multi_stdin() {
    for d in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[d, "\t\n", "-", "-"])
            .pipe_in_fixture("html_colors.txt")
            .succeeds()
            .stdout_is_fixture("html_colors.expected");
    }
}

#[test]
// TODO: make this test work on Windows
#[cfg(not(windows))]
fn test_delimiter_list_ending_with_escaped_backslash() {
    for d in ["-d", "--delimiters"] {
        let (at, mut ucmd) = at_and_ucmd!();
        let mut ins = vec![];
        for (i, one_in) in ["a\n", "b\n"].iter().enumerate() {
            let file = format!("in{i}");
            at.write(&file, one_in);
            ins.push(file);
        }
        ucmd.args(&[d, r"\\"])
            .args(&ins)
            .succeeds()
            .stdout_is("a\\b\n");
    }
}

#[test]
fn test_delimiter_list_ending_with_unescaped_backslash() {
    for d in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[d, r"\"])
            .fails()
            .stderr_contains(r"delimiter list ends with an unescaped backslash: \");

        new_ucmd!()
            .args(&[d, r"\\\"])
            .fails()
            .stderr_contains(r"delimiter list ends with an unescaped backslash: \\\");

        new_ucmd!()
            .args(&[d, r"_\"])
            .fails()
            .stderr_contains(r"delimiter list ends with an unescaped backslash: _\");
    }
}

#[test]
fn test_delimiter_list_empty() {
    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, "", "-s"])
            .pipe_in(
                "\
A ALPHA 1 _
B BRAVO 2 _
C CHARLIE 3 _
",
            )
            .succeeds()
            .stdout_only(
                "\
A ALPHA 1 _B BRAVO 2 _C CHARLIE 3 _
",
            );
    }
}

// Was panicking (usize subtraction that would have resulted in a negative number)
// Not observable in release builds, since integer overflow checking is not enabled
#[test]
fn test_delimiter_truncation() {
    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, "!@#", "-s", "-", "-", "-"])
            .pipe_in(
                "\
FIRST
SECOND
THIRD
FOURTH
ABCDEFG
",
            )
            .succeeds()
            .stdout_only(
                "\
FIRST!SECOND@THIRD#FOURTH!ABCDEFG


",
            );
    }
}

#[test]
#[cfg(unix)]
fn test_non_utf8_input() {
    // 0xC0 is not valid UTF-8
    const INPUT: &[u8] = b"Non-UTF-8 test: \xC0\x00\xC0.\n";

    new_ucmd!()
        .pipe_in(INPUT)
        .succeeds()
        .stdout_only_bytes(INPUT);
}

#[test]
fn test_three_trailing_backslashes_delimiter() {
    const ONE_BACKSLASH_STR: &str = r"\";

    let three_backslashes_string = ONE_BACKSLASH_STR.repeat(3);

    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, &three_backslashes_string])
            .fails()
            .no_stdout()
            .stderr_str_check(|st| {
                st.ends_with(&format!(
                    ": delimiter list ends with an unescaped backslash: {three_backslashes_string}\n"
                ))
            });
    }
}

// "If any other characters follow the <backslash>, the results are unspecified."
// https://pubs.opengroup.org/onlinepubs/9799919799/utilities/paste.html
// However, other implementations remove the backslash
#[test]
fn test_posix_unspecified_delimiter() {
    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, r"\z", "-s"])
            .pipe_in(
                "\
1
2
3
4
",
            )
            .succeeds()
            .stdout_only(
                "\
1z2z3z4
",
            );
    }
}

// "Empty string (not a null character)"
// https://pubs.opengroup.org/onlinepubs/9799919799/utilities/paste.html
#[test]
fn test_backslash_zero_delimiter() {
    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, r"\0z\0", "-s"])
            .pipe_in(
                "\
1
2
3
4
5
6
",
            )
            .succeeds()
            .stdout_only(
                "\
12z345z6
",
            );
    }
}

#[test]
fn test_gnu_escape_sequences() {
    let cases: &[(&str, u8)] = &[(r"\b", 0x08), (r"\f", 0x0C), (r"\r", 0x0D), (r"\v", 0x0B)];
    for &(esc, byte) in cases {
        let expected = [b'1', byte, b'2', byte, b'3', b'\n'];
        new_ucmd!()
            .args(&["-s", "-d", esc])
            .pipe_in("1\n2\n3\n")
            .succeeds()
            .stdout_only_bytes(expected);
    }
}

// As of 2024-10-09, only bsdutils (https://github.com/dcantrell/bsdutils, derived from FreeBSD) and toybox handle
// multibyte delimiter characters in the way a user would likely expect. BusyBox and GNU Core Utilities do not.
#[test]
fn test_multi_byte_delimiter() {
    for option_style in ["-d", "--delimiters"] {
        new_ucmd!()
            .args(&[option_style, "!ß@", "-s"])
            .pipe_in(
                "\
1
2
3
4
5
6
",
            )
            .succeeds()
            .stdout_only(
                "\
1!2ß3@4!5ß6
",
            );
    }
}

#[test]
fn test_data() {
    for example in EXAMPLE_DATA {
        let (at, mut ucmd) = at_and_ucmd!();
        let mut ins = vec![];
        for (i, one_in) in example.ins.iter().enumerate() {
            let file = format!("in{i}");
            at.write(&file, one_in);
            ins.push(file);
        }
        println!("{}", example.name);
        ucmd.args(example.args)
            .args(&ins)
            .succeeds()
            .stdout_is(example.out);
    }
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")]
fn test_non_utf8_delimiter() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.write("f1", "1\n2\n");
    at.write("f2", "a\nb\n");
    let delim = std::ffi::OsString::from_vec(vec![0xA2, 0xE3]);
    ucmd.env("LC_ALL", "zh_CN.gb18030")
        .arg("-d")
        .arg(&delim)
        .args(&["f1", "f2"])
        .succeeds()
        .stdout_only_bytes(b"1\xA2\xE3a\n2\xA2\xE3b\n");
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")]
fn test_paste_non_utf8_paths() {
    let (at, mut ucmd) = at_and_ucmd!();

    let filename1 = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]);
    let filename2 = std::ffi::OsString::from_vec(vec![0xF0, 0x90]);

    std::fs::write(at.plus(&filename1), b"line1\nline2\n").unwrap();
    std::fs::write(at.plus(&filename2), b"col1\ncol2\n").unwrap();

    ucmd.arg(&filename1)
        .arg(&filename2)
        .succeeds()
        .stdout_is("line1\tcol1\nline2\tcol2\n");
}

#[cfg(target_os = "linux")]
fn make_broken_pipe() -> std::io::PipeWriter {
    let (read, write) = std::io::pipe().expect("Failed to create pipe");
    // Drop the read end so writes fail with EPIPE.
    drop(read);
    // Return the write end of the pipe
    write
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/dev) not visible")]
fn test_dev_zero_write_error_dev_full() {
    use std::fs::File;

    let dev_full =
        File::create("/dev/full").expect("Failed to open /dev/full - test must run on Linux");

    new_ucmd!()
        .arg("/dev/zero")
        .set_stdout(dev_full)
        .fails()
        .code_is(1)
        .stderr_contains("No space left on device");
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths (/dev) not visible")]
fn test_dev_zero_closed_pipe() {
    new_ucmd!()
        .arg("/dev/zero")
        .set_stdout(make_broken_pipe())
        .run()
        .fails_silently();
}
