use rex::{
    engine::{Engine, Module, Value},
    parser::{ParseError, parse as parse_rex},
    typesystem::{BuiltinTypeId, Type},
};

fn format_parse_errors(errs: &[ParseError]) -> String {
    let mut out = String::from("parse error:");
    for err in errs {
        out.push_str(&format!("\n  {err}"));
    }
    out
}

async fn assert_program_ok(name: &str, source: &str, expected_value: i32, expected_type: Type) {
    let program =
        parse_rex(source).unwrap_or_else(|errs| panic!("{name}:\n{}", format_parse_errors(&errs)));

    let mut engine = Engine::with_prelude(()).unwrap();
    let mut module = Module::global();
    module.add_decls(program.decls.clone());
    engine
        .inject_module(module)
        .unwrap_or_else(|err| panic!("{name}: engine decl error: {err}"));
    let (value, ty) = engine
        .into_evaluator()
        .eval(program.body.as_ref().unwrap().as_ref())
        .await
        .unwrap_or_else(|err| panic!("{name}: eval error: {err}"));
    assert_eq!(ty, expected_type, "{name}: unexpected eval type");

    match value
        .value()
        .unwrap_or_else(|err| panic!("{name}: heap read error: {err}"))
    {
        Value::I32(actual) => assert_eq!(actual, expected_value, "{name}: unexpected eval value"),
        _ => panic!("{name}: expected i32 result"),
    }
}

#[tokio::test]
async fn example_adt_record_constructor() {
    assert_program_ok(
        "adt_record_constructor",
        r#"
            type Foo = Bar { x: i32, y: i32 };

            let v: Foo = Bar { x = 1, y = 2 } in
              v.x + v.y
        "#,
        3,
        Type::builtin(BuiltinTypeId::I32),
    )
    .await;
}

#[tokio::test]
async fn example_nested_lets() {
    assert_program_ok(
        "nested_lets",
        r#"
            let
              a = 1,
              b = 2,
              c = a + b
            in c
        "#,
        3,
        Type::builtin(BuiltinTypeId::I32),
    )
    .await;
}

#[tokio::test]
async fn example_lambda_application() {
    assert_program_ok(
        "lambda_application",
        r#"
            let inc = \x -> x + 1 in
              inc 41
        "#,
        42,
        Type::builtin(BuiltinTypeId::I32),
    )
    .await;
}

#[tokio::test]
async fn example_match() {
    assert_program_ok(
        "match",
        r#"
            type Sum = A { x: i32 } | B { x: i32 };

            let v: Sum = A { x = 7 } in
              match v with {
                case A {x} -> x;
                case B {x} -> x + 100;
              }
        "#,
        7,
        Type::builtin(BuiltinTypeId::I32),
    )
    .await;
}
