use anyhow::{bail, Result};
use wasmtime::*;

const MODULE: &'static str = r#"
    (module
      (import "" "" (func $add (param i32 i32) (result i32)))
      (func $test (result i32)
        (i32.const 42)
      )

      (func $sum10 (param $arg_1 i32) (param $arg_2 i32) (param $arg_3 i32) (param $arg_4 i32) (param $arg_5 i32) (param $arg_6 i32) (param $arg_7 i32) (param $arg_8 i32) (param $arg_9 i32) (param $arg_10 i32) (result i32)
        local.get $arg_1
        local.get $arg_2
        i32.add
        local.get $arg_3
        i32.add
        local.get $arg_4
        i32.add
        local.get $arg_5
        i32.add
        local.get $arg_6
        i32.add
        local.get $arg_7
        i32.add
        local.get $arg_8
        i32.add
        local.get $arg_9
        i32.add
        local.get $arg_10
        i32.add)

      (func $call_add (param i32 i32) (result i32)
        (local.get 0)
        (local.get 1)
        (call $add))

      (export "42" (func $test))
      (export "sum10" (func $sum10))
      (export "call_add" (func $call_add))
    )
    "#;

fn add_fn(store: impl AsContextMut) -> Func {
    Func::wrap(store, |a: i32, b: i32| a + b)
}

#[test]
#[cfg_attr(miri, ignore)]
fn array_to_wasm() -> Result<()> {
    let mut c = Config::new();
    c.strategy(Strategy::Winch);
    let engine = Engine::new(&c)?;
    let mut store = Store::new(&engine, ());
    let module = Module::new(&engine, MODULE)?;

    let add_fn = add_fn(store.as_context_mut());
    let instance = Instance::new(&mut store, &module, &[add_fn.into()])?;

    let constant = instance
        .get_func(&mut store, "42")
        .ok_or(anyhow::anyhow!("test function not found"))?;
    let mut returns = vec![Val::null(); 1];
    constant.call(&mut store, &[], &mut returns)?;

    assert_eq!(returns.len(), 1);
    assert_eq!(returns[0].unwrap_i32(), 42);

    let sum = instance
        .get_func(&mut store, "sum10")
        .ok_or(anyhow::anyhow!("sum10 function not found"))?;
    let mut returns = vec![Val::null(); 1];
    let args = vec![Val::I32(1); 10];
    sum.call(&mut store, &args, &mut returns)?;

    assert_eq!(returns.len(), 1);
    assert_eq!(returns[0].unwrap_i32(), 10);
    Ok(())
}

#[test]
#[cfg_attr(miri, ignore)]
fn native_to_wasm() -> Result<()> {
    let mut c = Config::new();
    c.strategy(Strategy::Winch);
    let engine = Engine::new(&c)?;
    let mut store = Store::new(&engine, ());
    let module = Module::new(&engine, MODULE)?;

    let add_fn = add_fn(store.as_context_mut());
    let instance = Instance::new(&mut store, &module, &[add_fn.into()])?;

    let f = instance.get_typed_func::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32), i32>(
        &mut store, "sum10",
    )?;

    let args = (1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
    let result = f.call(&mut store, args)?;

    assert_eq!(result, 10);
    Ok(())
}

#[test]
#[cfg_attr(miri, ignore)]
fn wasm_to_native() -> Result<()> {
    let mut c = Config::new();
    c.strategy(Strategy::Winch);
    let engine = Engine::new(&c)?;
    let mut store = Store::new(&engine, ());

    let module = Module::new(&engine, MODULE)?;
    let add_fn = add_fn(store.as_context_mut());

    let instance = Instance::new(&mut store, &module, &[add_fn.into()])?;

    let f = instance.get_typed_func::<(i32, i32), i32>(&mut store, "call_add")?;

    let args = (41, 1);
    let result = f.call(&mut store, args)?;

    assert_eq!(result, 42);
    Ok(())
}

#[test]
#[cfg_attr(miri, ignore)]
#[cfg_attr(windows, ignore)]
// NB
//
// This and the following test(`native_to_wasm_trap` and `wasm_to_native_trap`),
// are mostly smoke tests to ensure Winch's trampolines are compliant with fast
// stack walking. The ideal state is one in which we should not have to worry
// about testing the backtrace implementation per compiler, but instead be
// certain that a single set of test cases is enough to ensure that the machine
// code generated by Winch and Cranelift is compliant. One way to achieve this
// could be to share the implementation of trampolines between Cranelift and
// Winch.
//
// FIXME The following two tests are also temporarily ignored on Windows, since
// we are not emitting the require unwind information yet.
fn native_to_wasm_trap() -> Result<()> {
    let mut c = Config::new();
    c.strategy(Strategy::Winch);
    let engine = Engine::new(&c)?;
    let wat = r#"
        (module
          (func $div_by_zero (result i32)
            (i32.const 1)
            (i32.const 0)
            i32.div_u)

          (export "div_by_zero" (func $div_by_zero)))
    "#;
    let mut store = Store::new(&engine, ());
    let module = Module::new(&engine, wat)?;
    let instance = Instance::new(&mut store, &module, &[])?;
    let f = instance.get_typed_func::<(), i32>(&mut store, "div_by_zero")?;
    let result = f.call(&mut store, ()).unwrap_err();

    assert!(result.downcast_ref::<WasmBacktrace>().is_some());

    Ok(())
}

#[test]
#[cfg_attr(miri, ignore)]
#[cfg_attr(windows, ignore)]
fn wasm_to_native_trap() -> Result<()> {
    let mut c = Config::new();
    c.strategy(Strategy::Winch);
    let engine = Engine::new(&c)?;
    let wat = r#"
        (module
          (import "" "" (func $fail))
          (func $call_fail
            call $fail)

          (export "call_fail" (func $call_fail)))
    "#;
    let mut store = Store::new(&engine, ());
    let module = Module::new(&engine, wat)?;
    let func = Func::wrap::<(), (), Result<()>>(&mut store, || bail!("error"));
    let instance = Instance::new(&mut store, &module, &[func.into()])?;
    let f = instance.get_typed_func::<(), ()>(&mut store, "call_fail")?;
    let result = f.call(&mut store, ()).unwrap_err();

    assert!(result.downcast_ref::<WasmBacktrace>().is_some());

    Ok(())
}
