use std::fs::read_to_string;
use std::path::PathBuf;
use std::process::Command;

static CBINDGEN_PATH: &str = env!("CARGO_BIN_EXE_cbindgen");

fn test_project(project_path: &str) {
    let mut cmake_cmd = Command::new("cmake");
    cmake_cmd.arg("--version");
    cmake_cmd
        .output()
        .expect("CMake --version failed - Is CMake installed?");

    let mut cmake_configure = Command::new("cmake");
    let build_dir = PathBuf::from(project_path).join("build");
    if build_dir.exists() {
        std::fs::remove_dir_all(&build_dir).expect("Failed to remove old build directory");
    }
    let project_dir = PathBuf::from(project_path);

    let cbindgen_define = format!("-DCBINDGEN_PATH={CBINDGEN_PATH}");
    cmake_configure
        .arg("-S")
        .arg(project_path)
        .arg("-B")
        .arg(&build_dir)
        .arg(cbindgen_define);
    let output = cmake_configure.output().expect("Failed to execute process");
    let stdout_str = String::from_utf8(output.stdout).unwrap();
    let stderr_str = String::from_utf8(output.stderr).unwrap();
    assert!(
        output.status.success(),
        "Configuring test project failed: stdout: `{stdout_str}`, stderr: `{stderr_str}`"
    );
    let depfile_path = build_dir.join("depfile.d");
    assert!(
        !depfile_path.exists(),
        "depfile should not exist before building"
    );

    // Do the clean first build
    let mut cmake_build = Command::new("cmake");
    cmake_build.arg("--build").arg(&build_dir);
    let output = cmake_build.output().expect("Failed to execute process");
    assert!(
        output.status.success(),
        "Building test project failed: {output:?}"
    );
    let out_str = String::from_utf8(output.stdout).unwrap();
    assert!(
        out_str.contains("Running cbindgen"),
        "cbindgen rule did not run. Output: {out_str}"
    );

    assert!(
        depfile_path.exists(),
        "depfile does not exist after building"
    );

    let expected_dependencies_filepath = PathBuf::from(project_path)
        .join("expectations")
        .join("dependencies");
    assert!(
        expected_dependencies_filepath.exists(),
        "Test did not define expected dependencies. Please read the Readme.md"
    );
    let expected_deps =
        read_to_string(expected_dependencies_filepath).expect("Failed to read dependencies");
    let depinfo = read_to_string(depfile_path).expect("Failed to read dependencies");
    // Assumes a single rule in the file - all deps are listed to the rhs of the `:`.
    let actual_deps = depinfo.split(':').collect::<Vec<_>>()[1];
    // Strip the line breaks.
    let actual_deps = actual_deps.replace("\\\n", " ");
    // I don't want to deal with supporting escaped whitespace when splitting at whitespace,
    // so the tests don't support being run in a directory containing whitespace.
    assert!(
        !actual_deps.contains("\\ "),
        "The tests directory may not contain any whitespace"
    );
    let dep_list: Vec<&str> = actual_deps.split_ascii_whitespace().collect();
    let expected_dep_list: Vec<String> = expected_deps
        .lines()
        .map(|dep| project_dir.join(dep).to_str().unwrap().to_string())
        .collect();
    assert_eq!(dep_list, expected_dep_list);

    let output = cmake_build.output().expect("Failed to execute process");
    assert!(
        output.status.success(),
        "Building test project failed: {output:?}"
    );
    let out_str = String::from_utf8(output.stdout).unwrap();
    assert!(
        !out_str.contains("Running cbindgen"),
        "cbindgen rule ran on second build"
    );

    std::fs::remove_dir_all(build_dir).expect("Failed to remove old build directory");
}

macro_rules! test_file {
    ($test_function_name:ident, $name:expr, $file:tt) => {
        #[test]
        fn $test_function_name() {
            test_project($file);
        }
    };
}

// This file is generated by build.rs
include!(concat!(env!("OUT_DIR"), "/depfile_tests.rs"));
