// Copyright 2023 Martin Pool

//! Tests for error value mutations, from `--error-value` etc.

use std::{
    env,
    fs::{create_dir_all, rename},
};

use indoc::indoc;
use predicates::prelude::*;

mod util;
use tempfile::TempDir;
use util::{copy_of_testdata, run};

#[test]
fn error_value_catches_untested_ok_case() {
    // By default this tree should fail because it's configured to
    // generate an error value, and the tests forgot to check that
    // the code under test does return Ok.
    let tmp = copy_of_testdata("error_value");
    run()
        .arg("mutants")
        .args(["-v", "-V", "--no-times", "--no-shuffle"])
        .arg("-d")
        .arg(tmp.path())
        .assert()
        .code(2)
        .stderr("");
}

#[test]
fn no_config_option_disables_config_file_so_error_value_is_not_generated() {
    // In this case, the config file is not loaded. Error values are not
    // generated by default (because we don't know what a good value for
    // this tree would be), so no mutants are caught.
    let tmp_src_dir = copy_of_testdata("error_value");
    run()
        .arg("mutants")
        .args(["-v", "-V", "--no-times", "--no-shuffle", "--no-config"])
        .arg("-d")
        .arg(tmp_src_dir.path())
        .assert()
        .code(0)
        .stderr("")
        .stdout(predicate::function(|stdout: &str| {
            insta::assert_snapshot!(stdout);
            true
        }));
}

#[test]
fn list_mutants_with_error_value_from_command_line_list() {
    // This is not a good error mutant for this tree, which uses
    // anyhow, but it's a good test of the command line option.
    let tmp_src_dir = copy_of_testdata("error_value");
    run()
        .arg("mutants")
        .args([
            "--no-times",
            "--no-shuffle",
            "--no-config",
            "--list",
            "--error=::eyre::eyre!(\"mutant\")",
        ])
        .arg("-d")
        .arg(tmp_src_dir.path())
        .assert()
        .code(0)
        .stderr("")
        .stdout(predicate::function(|stdout: &str| {
            insta::assert_snapshot!(stdout);
            true
        }));
}

#[test]
fn warn_if_error_value_starts_with_err() {
    // Users might misunderstand what should be passed to --error,
    // so give a warning.
    let tmp_src_dir = copy_of_testdata("error_value");
    run()
        .arg("mutants")
        .args([
            "--no-times",
            "--no-shuffle",
            "--no-config",
            "--list",
            "--error=Err(anyhow!(\"mutant\"))",
        ])
        .arg("-d")
        .arg(tmp_src_dir.path())
        .assert()
        .code(0)
        .stderr(predicate::str::contains(
            "error_value option gives the value of the error, and probably should not start with Err(: got Err(anyhow!(\"mutant\"))"
        ));
}

#[test]
fn warn_unresolved_module() {
    let tmp_src_dir = copy_of_testdata("dangling_mod");
    run()
        .arg("mutants")
        .args(["--no-times", "--no-shuffle", "--no-config", "--list"])
        .arg("-d")
        .arg(tmp_src_dir.path())
        .assert()
        .code(0)
        .stderr(predicate::str::contains(
            r#"referent of mod not found definition_site="src/main.rs:3:1" mod_name=nonexistent"#,
        ));
}
#[test]
fn warn_module_outside_of_tree() {
    // manually copy tree, so that external path still resolves correctly for `cargo`
    //
    // [TEMP]/dangling_mod/*
    // [TEMP]/nested_mod/src/paths_in_main/a/foo.rs
    //
    let tree_name = "dangling_mod";
    let tmp_src_dir_parent = TempDir::with_prefix("warn_module_outside_of_tree").unwrap();
    let tmp_src_dir = tmp_src_dir_parent.path().join("dangling_mod");
    cp_r::CopyOptions::new()
        .filter(|path, _stat| {
            Ok(["target", "mutants.out", "mutants.out.old"]
                .iter()
                .all(|p| !path.starts_with(p)))
        })
        .copy_tree(
            std::path::Path::new("testdata").join(tree_name),
            &tmp_src_dir,
        )
        .unwrap();
    rename(
        tmp_src_dir.join("Cargo_test.toml"),
        tmp_src_dir.join("Cargo.toml"),
    )
    .unwrap();

    let external_file_path = tmp_src_dir_parent
        .path()
        .join("nested_mod/src/paths_in_main/a");
    create_dir_all(&external_file_path).unwrap();
    std::fs::copy(
        std::path::Path::new("testdata/nested_mod/src/paths_in_main/a/foo.rs"),
        external_file_path.join("foo.rs"),
    )
    .unwrap();

    run()
        .arg("mutants")
        .args(["--no-times", "--no-shuffle", "--no-config", "--list"])
        .arg("-d")
        .arg(tmp_src_dir)
        .assert()
        .code(0)
        .stderr(predicate::str::contains(
            r#"skipping source outside of tree: "src/../../nested_mod/src/paths_in_main/a/foo.rs""#,
        ));
}

#[test]
fn fail_when_error_value_does_not_parse() {
    let tmp_src_dir = copy_of_testdata("error_value");
    run()
        .arg("mutants")
        .args([
            "--no-times",
            "--no-shuffle",
            "--no-config",
            "--list",
            "--error=shouldn't work",
        ])
        .arg("-d")
        .arg(tmp_src_dir.path())
        .assert()
        .code(1)
        .stderr(predicate::str::contains(indoc! { "
            Error: Failed to parse error value \"shouldn\'t work\"

            Caused by:
                unexpected token
        "}))
        .stdout(predicate::str::is_empty());
}
