//! You can run this test suite with:
//!
//!     cargo test --test all
//!
//! An argument can be passed as well to filter, based on filename, which test
//! to run
//!
//!     cargo test --test all foo.wit

use anyhow::{Context, Result, bail};
use libtest_mimic::{Arguments, Trial};
use pretty_assertions::StrComparison;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::str;
use wit_parser::*;

fn main() {
    env_logger::init();
    let tests = find_tests();

    let mut trials = Vec::new();
    for test in tests {
        let trial = Trial::test(format!("{test:?}"), move || {
            Runner {}
                .run(&test)
                .context(format!("test {test:?} failed"))
                .map_err(|e| format!("{e:?}").into())
        });
        trials.push(trial);
    }

    let mut args = Arguments::from_args();
    if cfg!(target_family = "wasm") && !cfg!(target_feature = "atomics") {
        args.test_threads = Some(1);
    }
    libtest_mimic::run(&args, trials).exit();
}

/// Recursively finds all tests in a whitelisted set of directories which we
/// then load up and test in parallel.
fn find_tests() -> Vec<PathBuf> {
    let mut tests = Vec::new();
    find_tests("tests/ui".as_ref(), &mut tests);
    find_tests("tests/ui/parse-fail".as_ref(), &mut tests);
    tests.sort();
    return tests;

    fn find_tests(path: &Path, tests: &mut Vec<PathBuf>) {
        for f in path.read_dir().unwrap() {
            let f = f.unwrap();
            let path = f.path();
            if path.file_name().unwrap().to_str().unwrap() == "parse-fail" {
                continue;
            }
            if f.file_type().unwrap().is_dir() {
                tests.push(path);
                continue;
            }

            match path.extension().and_then(|s| s.to_str()) {
                Some("md") | Some("wit") | Some("wat") | Some("wasm") => {}
                _ => continue,
            }
            tests.push(path);
        }
    }
}

struct Runner {}

impl Runner {
    fn run(&mut self, test: &Path) -> Result<()> {
        let mut resolve = Resolve::new();
        resolve.features.insert("active".to_string());
        let result = resolve.push_path(test);
        let result = if test.iter().any(|s| s == "parse-fail") {
            match result {
                Ok(_) => bail!("expected test to not parse but it did"),
                Err(mut e) => {
                    if let Some(err) = e.downcast_mut::<io::Error>() {
                        *err = io::Error::new(
                            io::ErrorKind::Other,
                            "some generic platform-agnostic error message",
                        );
                    }
                    format!("{e:#}")
                }
            }
        } else {
            result?;
            // format json string to human readable
            let json_result = serde_json::to_string_pretty(&resolve)?;
            // "foo.wit" => "foo.wit.json"
            self.read_or_write_to_file(test, &json_result, "json")?;
            return Ok(());
        };

        // "foo.wit" => "foo.wit.result"
        // "foo.wit.md" => "foo.wit.md.result"
        self.read_or_write_to_file(test, &result, "result")?;
        return Ok(());
    }

    fn read_or_write_to_file(
        &mut self,
        test: &Path,
        result: &str,
        extension: &str,
    ) -> Result<(), anyhow::Error> {
        let result_file = if test.extension() == Some(OsStr::new("md"))
            && test
                .file_stem()
                .and_then(|path| Path::new(path).extension())
                == Some(OsStr::new("wit"))
        {
            test.with_extension(format!("md.{extension}"))
        } else {
            test.with_extension(format!("wit.{extension}"))
        };
        if env::var_os("BLESS").is_some() {
            let normalized = normalize(&result, extension);
            fs::write(&result_file, normalized)?;
        } else {
            let expected = fs::read_to_string(&result_file).context(format!(
                "failed to read test expectation file {result_file:?}\nthis can be fixed with BLESS=1"
            ))?;
            let expected = normalize(&expected, extension);
            let result = normalize(&result, extension);
            if expected != result {
                bail!(
                    "failed test: result is not as expected:{}",
                    StrComparison::new(&expected, &result),
                );
            }
        }
        Ok(())
    }
}

fn normalize(s: &str, extension: &str) -> String {
    let s = s.trim();
    match extension {
        // .result files have error messages with paths, so normalize Windows \ separators to /
        "result" => s.replace("\\", "/").replace("\r\n", "\n"),

        // .json files escape strings with \, so leave them alone
        _ => s.replace("\r\n", "\n"),
    }
}
