use super::{Module, TypedFunc};
use crate::{runtime::OptCtx, value::Verdict};

pub struct TestCase<Ctx: 'static> {
    name: String,
    func: TypedFunc<Ctx, fn() -> Verdict<(), ()>>,
}

impl<C: OptCtx> TestCase<C> {
    pub fn new(
        name: String,
        func: TypedFunc<C, fn() -> Verdict<(), ()>>,
    ) -> Self {
        Self { name, func }
    }

    pub fn name(&self) -> &str {
        &self.name
    }
}

impl<C: OptCtx> TestCase<C> {
    pub fn run(&self, ctx: &mut C::Ctx) -> Result<(), ()> {
        match self.func.call_tuple(ctx, ()) {
            Verdict::Accept(()) => Ok(()),
            Verdict::Reject(()) => Err(()),
        }
    }
}

pub(crate) fn get_tests<Ctx: OptCtx>(
    module: &mut Module<Ctx>,
) -> impl Iterator<Item = TestCase<Ctx>> + use<'_, Ctx> {
    let mut tests: Vec<_> = module
        .functions
        .keys()
        .filter(|x| {
            x.rsplit_once(".")
                .map_or(x.as_ref(), |x| x.1)
                .starts_with("test#")
        })
        .map(Clone::clone)
        .collect();

    tests.sort();

    tests.into_iter().map(|name| {
        TestCase::new(
            name.replace("test#", ""),
            module
                .get_function::<fn() -> Verdict<(), ()>>(
                    name.strip_prefix("pkg.").unwrap(),
                )
                .unwrap(),
        )
    })
}

pub(crate) fn run_tests<Ctx: OptCtx>(
    module: &mut Module<Ctx>,
    mut ctx: Ctx,
) -> Result<(), ()> {
    let tests: Vec<_> = get_tests::<Ctx>(module).collect();

    let total = tests.len();
    let total_width = total.to_string().len();
    let mut successes = 0;
    let mut failures = 0;

    for (n, test) in tests.into_iter().enumerate() {
        let n = n + 1;
        let test_display = test.name();
        print!("Test {n:>total_width$} / {total}: {test_display}... ");

        if test.run(ctx.get_context()) == Ok(()) {
            successes += 1;
            println!("\x1B[92mok\x1B[m");
        } else {
            failures += 1;
            println!("\x1B[91mfail\x1B[m");
        }
    }
    println!("Ran {total} tests, {successes} succeeded, {failures} failed");

    if failures == 0 {
        Result::Ok(())
    } else {
        Result::Err(())
    }
}
