/*
 * Copyright (c) 2024 Jonathan Perkin <jonathan@perkin.org.uk>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};

const MKTOOL: &str = env!("CARGO_BIN_EXE_mktool");

/*
 * Invalid arguments.
 *
 * XXX: This is not fully compatible, checksum.awk exits 3, we exit 2, but
 * unfortunatly this is hardcoded in clap.
 */
#[test]
fn test_checksum_invalid_args() {
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(2));
}

/*
 * Test running with no input files.
 */
#[test]
fn test_checksum_no_input() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(distinfo)
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());
}

/*
 * Pass a nonexistent distinfo file.
 */
#[test]
fn test_checksum_bad_distinfo() {
    let distinfo = PathBuf::from("/nonexistent");
    let output =
        format!("checksum: distinfo file missing: {}\n", distinfo.display());
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(distinfo)
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(3));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());
}

/*
 * Passing a file that doesn't exist in distinfo should print a message
 * to stderr and exit 2.
 */
#[test]
fn test_checksum_bad_distfile() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("foo")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(2));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(
        cmd.stderr,
        "checksum: No checksum recorded for foo\n".as_bytes()
    );
}

/*
 * Tests against a valid full distfile test.
 */
#[test]
fn test_checksum_valid_distfile() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let output = format!(
        "{}\n{}\n",
        "=> Checksum BLAKE2s OK for digest1.txt",
        "=> Checksum SHA512 OK for digest1.txt"
    );
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());

    /*
     * Identical to previous except the path to the distfile is
     * different (but still valid).
     */
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("../data/digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());
    /*
     * Valid, but we only request a single algorithm each time.
     */
    let output = "=> Checksum BLAKE2s OK for digest1.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-a")
        .arg("BLAKE2s")
        .arg(&distinfo)
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());

    let output = "=> Checksum SHA512 OK for digest2.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-a")
        .arg("SHA512")
        .arg(&distinfo)
        .arg("digest2.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());

    /*
     * Output should be in distinfo order, with errors last, regardless of
     * order of command line arguments.
     */
    let output = format!(
        "{}\n{}\n{}\n{}\n",
        "=> Checksum BLAKE2s OK for digest1.txt",
        "=> Checksum SHA512 OK for digest1.txt",
        "=> Checksum BLAKE2s OK for digest2.txt",
        "=> Checksum SHA512 OK for digest2.txt"
    );
    let outerr = "checksum: No checksum recorded for digest11.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("digest2.txt")
        .arg("digest11.txt")
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(2));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, outerr.as_bytes());
}

/*
 * Test input from file / stdin.
 */
#[test]
fn test_checksum_input_file() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
    let tmpfile = tmpdir.join("test_checksum_input_file.txt");
    fs::write(&tmpfile, "digest2.txt\n").expect("unable to write temp file");
    let output = format!(
        "{}\n{}\n{}\n{}\n",
        "=> Checksum BLAKE2s OK for digest1.txt",
        "=> Checksum SHA512 OK for digest1.txt",
        "=> Checksum BLAKE2s OK for digest2.txt",
        "=> Checksum SHA512 OK for digest2.txt"
    );
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("-I")
        .arg(&tmpfile)
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to spawn {}", MKTOOL));
    fs::remove_file(&tmpfile).expect("unable to remove temp file");
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());
}

#[test]
fn test_checksum_input_stdin() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let output = format!(
        "{}\n{}\n{}\n{}\n",
        "=> Checksum BLAKE2s OK for digest1.txt",
        "=> Checksum SHA512 OK for digest1.txt",
        "=> Checksum BLAKE2s OK for digest2.txt",
        "=> Checksum SHA512 OK for digest2.txt"
    );
    let mut cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("-I")
        .arg("-")
        .arg("digest2.txt")
        .current_dir("tests/data")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap_or_else(|_| panic!("unable to spawn {}", MKTOOL));
    let mut stdin = cmd.stdin.take().expect("failed to open stdin");
    std::thread::spawn(move || {
        stdin
            .write_all("digest1.txt".as_bytes())
            .expect("failed to write to stdin");
    });
    let out = cmd.wait_with_output().expect("failed to wait on child");
    assert_eq!(out.status.code(), Some(0));
    assert_eq!(out.stdout, output.as_bytes());
    assert_eq!(out.stderr, "".as_bytes());
}

/*
 * Test strip suffix mode.  Use filename digest1.txt.suffix but validate
 * against the digest1.txt distinfo entry.
 */
#[test]
fn test_checksum_strip_mode() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");

    let output = "=> Checksum SHA512 OK for digest1.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-a")
        .arg("SHA512")
        .arg("-s")
        .arg(".suffix")
        .arg(&distinfo)
        .arg("digest1.txt.suffix")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());

    /* Invalid suffix strip.  */
    let output = "checksum: No checksum recorded for digest1.txt.suffix\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-s")
        .arg(".badsuffix")
        .arg(&distinfo)
        .arg("digest1.txt.suffix")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(2));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());
}

/*
 * No checksum type recorded.
 */
#[test]
fn test_checksum_no_checksum() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let output = format!(
        "{}\n{}\n",
        "checksum: No SHA1 checksum recorded for digest1.txt",
        "checksum: No SHA1 checksum recorded for nonexistent.txt",
    );
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-a")
        .arg("SHA1")
        .arg(&distinfo)
        .arg("digest1.txt")
        .arg("nonexistent.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(2));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());
}

/*
 * Patch mode.
 */
#[test]
fn test_checksum_patch() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let output = "=> Checksum SHA1 OK for patch-Makefile\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-p")
        .arg(&distinfo)
        .arg("patch-Makefile")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());
}

/*
 * Patch mode where the path doesn't exactly match the distinfo entry,
 * ensuring that find_entry() functionality is tested.
 */
#[test]
fn test_checksum_patch_path() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo");
    let output = "=> Checksum SHA1 OK for patch-Makefile\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-p")
        .arg(&distinfo)
        .arg("../data/patch-Makefile")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(0));
    assert_eq!(cmd.stdout, output.as_bytes());
    assert_eq!(cmd.stderr, "".as_bytes());
}

/*
 * Now run some of the same tests but with a modified distinfo file containing
 * invalid checksums.
 */
#[test]
fn test_checksum_mismatch() {
    let mut distinfo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    distinfo.push("tests/data/distinfo.bad");

    // Only the first checksum mismatch is printed on failure.
    let output = "checksum: Checksum BLAKE2s mismatch for digest1.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg(&distinfo)
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(1));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());

    let output = "checksum: Checksum SHA512 mismatch for digest1.txt\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-a")
        .arg("SHA512")
        .arg(&distinfo)
        .arg("digest1.txt")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(1));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());

    let output = "checksum: Checksum SHA1 mismatch for patch-Makefile\n";
    let cmd = Command::new(MKTOOL)
        .arg("checksum")
        .arg("-p")
        .arg(&distinfo)
        .arg("patch-Makefile")
        .current_dir("tests/data")
        .output()
        .unwrap_or_else(|_| panic!("unable to exec {}", MKTOOL));
    assert_eq!(cmd.status.code(), Some(1));
    assert_eq!(cmd.stdout, "".as_bytes());
    assert_eq!(cmd.stderr, output.as_bytes());
}
