use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};

struct Test {
    repo: &'static str,
    name: &'static str,
    sha: &'static str,
    lock: Option<&'static str>,
    packages: &'static [&'static str],
    features: Option<&'static [&'static str]>,
    manifest_path: Option<&'static str>,
    /// `filters` are passed to libtest (i.e., after a `--` in the `cargo test` invocation).
    filters: &'static [&'static str],
}

const TEST_REPOS: &[Test] = &[
    Test {
        name: "iron",
        repo: "https://github.com/iron/iron",
        sha: "cf056ea5e8052c1feea6141e40ab0306715a2c33",
        lock: None,
        packages: &[],
        features: None,
        manifest_path: None,
        filters: &[],
    },
    Test {
        name: "ripgrep",
        repo: "https://github.com/BurntSushi/ripgrep",
        sha: "ced5b92aa93eb47e892bd2fd26ab454008721730",
        lock: None,
        packages: &[],
        features: None,
        manifest_path: None,
        filters: &[],
    },
    Test {
        name: "tokei",
        repo: "https://github.com/XAMPPRocky/tokei",
        sha: "fdf3f8cb279a7aeac0696c87e5d8b0cd946e4f9e",
        lock: None,
        packages: &[],
        features: None,
        manifest_path: None,
        filters: &[],
    },
    Test {
        name: "xsv",
        repo: "https://github.com/BurntSushi/xsv",
        sha: "3de6c04269a7d315f7e9864b9013451cd9580a08",
        lock: None,
        packages: &[],
        features: None,
        manifest_path: None,
        // Many tests here use quickcheck and some of them can fail randomly, so only run deterministic tests.
        filters: &[
            "test_flatten::",
            "test_fmt::",
            "test_headers::",
            "test_index::",
            "test_join::",
            "test_partition::",
            "test_search::",
            "test_select::",
            "test_slice::",
            "test_split::",
            "test_stats::",
            "test_table::",
        ],
    },
    Test {
        name: "servo",
        repo: "https://github.com/servo/servo",
        sha: "785a344e32db58d4e631fd3cae17fd1f29a721ab",
        lock: None,
        // Only test Stylo a.k.a. Quantum CSS, the parts of Servo going into Firefox.
        // This takes much less time to build than all of Servo and supports stable Rust.
        packages: &["selectors"],
        features: None,
        manifest_path: None,
        filters: &[],
    },
    Test {
        name: "diesel",
        repo: "https://github.com/diesel-rs/diesel",
        sha: "3db7c17c5b069656ed22750e84d6498c8ab5b81d",
        lock: None,
        packages: &[],
        // Test the embedded sqlite variant of diesel
        // This does not require any dependency to be present,
        // sqlite will be compiled as part of the build process
        features: Some(&["sqlite", "libsqlite3-sys/bundled"]),
        // We are only interested in testing diesel itself
        // not any other crate present in the diesel workspace
        // (This is required to set the feature flags above)
        manifest_path: Some("diesel/Cargo.toml"),
        filters: &[],
    },
];

fn main() {
    let args = env::args().collect::<Vec<_>>();
    let cargo = &args[1];
    let out_dir = Path::new(&args[2]);
    let cargo = &Path::new(cargo);

    for test in TEST_REPOS.iter().rev() {
        if args[3..].is_empty() || args[3..].iter().any(|s| s.contains(test.name)) {
            test_repo(cargo, out_dir, test);
        }
    }
}

fn test_repo(cargo: &Path, out_dir: &Path, test: &Test) {
    println!("testing {}", test.repo);
    let dir = clone_repo(test, out_dir);
    if let Some(lockfile) = test.lock {
        fs::write(&dir.join("Cargo.lock"), lockfile).unwrap();
    }
    if !run_cargo_test(cargo, &dir, test.packages, test.features, test.manifest_path, test.filters)
    {
        panic!("tests failed for {}", test.repo);
    }
}

fn clone_repo(test: &Test, out_dir: &Path) -> PathBuf {
    let out_dir = out_dir.join(test.name);

    if !out_dir.join(".git").is_dir() {
        let status = Command::new("git").arg("init").arg(&out_dir).status().unwrap();
        assert!(status.success());
    }

    // Try progressively deeper fetch depths to find the commit
    let mut found = false;
    for depth in &[0, 1, 10, 100, 1000, 100000] {
        if *depth > 0 {
            let status = Command::new("git")
                .arg("fetch")
                .arg(test.repo)
                .arg(test.sha)
                .arg(&format!("--depth={}", depth))
                .current_dir(&out_dir)
                .status()
                .unwrap();
            assert!(status.success());
        }

        let status = Command::new("git")
            .arg("reset")
            .arg(test.sha)
            .arg("--hard")
            .current_dir(&out_dir)
            .status()
            .unwrap();

        if status.success() {
            found = true;
            break;
        }
    }

    if !found {
        panic!("unable to find commit {}", test.sha)
    }
    let status =
        Command::new("git").arg("clean").arg("-fdx").current_dir(&out_dir).status().unwrap();
    assert!(status.success());

    out_dir
}

fn run_cargo_test(
    cargo_path: &Path,
    crate_path: &Path,
    packages: &[&str],
    features: Option<&[&str]>,
    manifest_path: Option<&str>,
    filters: &[&str],
) -> bool {
    let mut command = Command::new(cargo_path);
    command.arg("test");

    if let Some(path) = manifest_path {
        command.arg(format!("--manifest-path={}", path));
    }

    if let Some(features) = features {
        command.arg("--no-default-features");
        for feature in features {
            command.arg(format!("--features={}", feature));
        }
    }

    for name in packages {
        command.arg("-p").arg(name);
    }

    command.arg("--");
    command.args(filters);

    let status = command
        // Disable rust-lang/cargo's cross-compile tests
        .env("CFG_DISABLE_CROSS_TESTS", "1")
        // Relax #![deny(warnings)] in some crates
        .env("RUSTFLAGS", "--cap-lints warn")
        // servo tries to use 'lld-link.exe' on windows, but we don't
        // have lld on our PATH in CI. Override it to use 'link.exe'
        .env("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER", "link.exe")
        .env("CARGO_TARGET_I686_PC_WINDOWS_MSVC_LINKER", "link.exe")
        .current_dir(crate_path)
        .status()
        .unwrap();

    status.success()
}
