use anyhow::{Context, Result};
use libtest_mimic::{Arguments, Trial};
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use wasmparser::{Validator, WasmFeatures};
use wit_parser::Resolve;

fn main() {
    let mut trials = Vec::new();
    // All tests are stored in `artifacts::TESTS` which is generated by
    // `crates/wit-dylib/test-programs/artifacts/build.rs` and
    // contains all caller/callee paired components.
    for (caller, callee, wit) in artifacts::TESTS {
        let caller = Path::new(caller);
        let callee = Path::new(callee);
        let wit = Path::new(wit);
        let test_name = wit.file_name().unwrap().to_str().unwrap();
        let trial = Trial::test(test_name.to_string(), move || {
            let mut tempdir = TempDir::new_in(caller.parent().unwrap()).unwrap();
            tempdir.disable_cleanup(true);
            run_test(&tempdir, caller, callee, wit)
                .with_context(|| {
                    format!(
                        "failed test {test_name:?}\nartifacts are in {:?}",
                        tempdir.path(),
                    )
                })
                .inspect(|_| tempdir.disable_cleanup(false))
                .map_err(|e| format!("{e:?}").into())
        })
        .with_ignored_flag(cfg!(target_family = "wasm"));
        trials.push(trial);
    }

    libtest_mimic::run(&Arguments::from_args(), trials).exit();
}

fn run_test(tempdir: &TempDir, caller: &Path, callee: &Path, wit: &Path) -> Result<()> {
    let mut resolve = Resolve::default();
    let package = resolve.push_file(wit).context("failed to load WIT")?;
    let caller_world = resolve.select_world(&[package], Some("caller"))?;
    let callee_world = resolve.select_world(&[package], Some("callee"))?;

    let composition_file = artifacts::compose(
        tempdir,
        &resolve,
        (caller, caller_world),
        (callee, callee_world),
    )?;

    // And finally run this component's `run` function in Wasmtime to ensure the
    // test passes.
    let mut cmd = Command::new("wasmtime");
    cmd.arg("run")
        .arg("--invoke=run()")
        .arg("-Shttp")
        .arg("-Wcomponent-model-async")
        .arg("-Wcomponent-model-error-context")
        .arg(&composition_file);
    let result = cmd.output().context("failed to run wasmtime")?;
    if result.status.success() {
        return Ok(());
    }

    let mut error = String::new();
    error.push_str(&format!("command: {cmd:?}\n"));
    error.push_str(&format!("status:  {}\n", result.status));
    if !result.stdout.is_empty() {
        error.push_str(&format!(
            "stdout:\n  {}",
            String::from_utf8_lossy(&result.stdout).replace("\n", "\n  ")
        ));
    }
    if !result.stderr.is_empty() {
        error.push_str(&format!(
            "stderr:\n  {}",
            String::from_utf8_lossy(&result.stderr).replace("\n", "\n  ")
        ));
    }

    if uses_async_and_wasmtime_does_not_support_async(&composition_file, &error)? {
        return Ok(());
    }

    anyhow::bail!("{error}")
}

fn uses_async_and_wasmtime_does_not_support_async(wasm: &Path, error: &str) -> Result<bool> {
    if !error.contains("invalid leading byte (0x43) for component defined type") {
        return Ok(false);
    }
    let wasm = std::fs::read(wasm)?;

    let validates_with_cm_async = Validator::new_with_features(WasmFeatures::all())
        .validate_all(&wasm)
        .is_ok();
    let validates_without_cm_async =
        Validator::new_with_features(WasmFeatures::all() ^ WasmFeatures::CM_ASYNC)
            .validate_all(&wasm)
            .is_ok();

    Ok(validates_with_cm_async && !validates_without_cm_async)
}
