use rex::{
    Rex,
    ast::{CompilationUnit, Symbol},
    engine::{Engine, Handle, Heap, Value},
    json::{json_to_rex, rex_to_json},
    parser::parse as parse_rex,
    typesystem::{AdtDecl, BuiltinTypeId, Type, TypeSystem, TypeVarSupply},
};
use serde::Serialize;
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;

fn mk_type_system() -> TypeSystem {
    TypeSystem::new_with_prelude().unwrap()
}

fn mk_unit_enum(name: &str, variants: &[&str]) -> AdtDecl {
    let mut supply = TypeVarSupply::new();
    let mut adt = AdtDecl::new(&Symbol::intern(name), &[], &mut supply);
    for variant in variants {
        adt.add_variant(Symbol::intern(variant), vec![]);
    }
    adt
}

fn temp_dir(name: &str) -> PathBuf {
    let mut dir = std::env::temp_dir();
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    dir.push(format!("rex-json-eval-{name}-{nanos}"));
    fs::create_dir_all(&dir).unwrap();
    dir
}

fn parse_program(source: &str) -> CompilationUnit {
    parse_rex(source).unwrap()
}

fn fixed_uuid() -> Uuid {
    Uuid::parse_str("12345678-1234-5678-90ab-cdef12345678").unwrap()
}

#[derive(Rex, Serialize)]
struct EvalJsonRecord {
    id: i32,
    values: Vec<i32>,
}

fn assert_eval_json(
    type_system: &TypeSystem,
    handle: &Handle,
    typ: &Type,
    expected: serde_json::Value,
) {
    let actual = rex_to_json(handle, typ, type_system).unwrap();
    assert_eq!(actual, expected);
}

#[test]
fn primitive_roundtrip() {
    let ts = mk_type_system();
    let heap = Heap::new();

    let cases = vec![
        (Type::builtin(BuiltinTypeId::Bool), json!(true)),
        (Type::builtin(BuiltinTypeId::I32), json!(-7)),
        (Type::builtin(BuiltinTypeId::String), json!("hello")),
    ];

    for (ty, expected_json) in cases {
        let handle = json_to_rex(&heap, &expected_json, &ty, &ts).unwrap();
        let actual_json = rex_to_json(&handle, &ty, &ts).unwrap();
        assert_eq!(actual_json, expected_json);
    }
}

#[test]
fn option_and_result_roundtrip() {
    let ts = mk_type_system();
    let heap = Heap::new();

    let opt_ty = Type::option(Type::builtin(BuiltinTypeId::I32));
    let some = json!(9);
    let none = serde_json::Value::Null;

    let some_handle = json_to_rex(&heap, &some, &opt_ty, &ts).unwrap();
    let none_handle = json_to_rex(&heap, &none, &opt_ty, &ts).unwrap();
    assert_eq!(rex_to_json(&some_handle, &opt_ty, &ts).unwrap(), some);
    assert_eq!(rex_to_json(&none_handle, &opt_ty, &ts).unwrap(), none);

    let res_ty = Type::result(
        Type::builtin(BuiltinTypeId::I32),
        Type::builtin(BuiltinTypeId::String),
    );
    let ok_json = json!({ "Ok": 1 });
    let err_json = json!({ "Err": "bad" });
    let ok_handle = json_to_rex(&heap, &ok_json, &res_ty, &ts).unwrap();
    let err_handle = json_to_rex(&heap, &err_json, &res_ty, &ts).unwrap();
    assert_eq!(rex_to_json(&ok_handle, &res_ty, &ts).unwrap(), ok_json);
    assert_eq!(rex_to_json(&err_handle, &res_ty, &ts).unwrap(), err_json);
}

#[test]
fn promise_roundtrip_from_json() {
    let ts = mk_type_system();
    let heap = Heap::new();
    let promise_ty = Type::promise(Type::builtin(BuiltinTypeId::I32));
    let promise_json = json!(fixed_uuid());

    let promise_handle = json_to_rex(&heap, &promise_json, &promise_ty, &ts).unwrap();
    let Value::Adt(tag, args) = promise_handle.value().unwrap() else {
        panic!("expected Promise ADT");
    };
    assert_eq!(tag.as_ref(), "Promise");
    assert_eq!(args.len(), 1);
    assert_eq!(args[0].to_rust::<Uuid>().unwrap(), fixed_uuid());
    assert_eq!(
        rex_to_json(&promise_handle, &promise_ty, &ts).unwrap(),
        promise_json
    );
}

#[test]
fn promise_roundtrip_from_runtime_value() {
    let ts = mk_type_system();
    let heap = Heap::new();
    let promise_ty = Type::promise(Type::builtin(BuiltinTypeId::String));
    let promise_id = heap.alloc_uuid(fixed_uuid()).unwrap();
    let promise_handle = heap
        .alloc_adt(Symbol::intern("Promise"), vec![promise_id])
        .unwrap();

    let promise_json = rex_to_json(&promise_handle, &promise_ty, &ts).unwrap();
    assert_eq!(promise_json, json!(fixed_uuid()));

    let roundtrip_handle = json_to_rex(&heap, &promise_json, &promise_ty, &ts).unwrap();
    let Value::Adt(tag, args) = roundtrip_handle.value().unwrap() else {
        panic!("expected Promise ADT");
    };
    assert_eq!(tag.as_ref(), "Promise");
    assert_eq!(args.len(), 1);
    assert_eq!(args[0].to_rust::<Uuid>().unwrap(), fixed_uuid());
}

#[test]
fn json_array_maps_to_array_not_list() {
    let ts = mk_type_system();
    let heap = Heap::new();
    let array_json = json!([1, 2, 3]);

    let array_ty = Type::array(Type::builtin(BuiltinTypeId::I32));
    let array_handle = json_to_rex(&heap, &array_json, &array_ty, &ts).unwrap();
    let Value::Array(items) = array_handle.value().unwrap() else {
        panic!("expected array");
    };
    assert_eq!(items.len(), 3);
    assert_eq!(
        rex_to_json(&array_handle, &array_ty, &ts).unwrap(),
        array_json
    );

    let list_ty = Type::list(Type::builtin(BuiltinTypeId::I32));
    let list_handle = json_to_rex(&heap, &array_json, &list_ty, &ts).unwrap();
    let Value::Adt(tag, _args) = list_handle.value().unwrap() else {
        panic!("expected list ADT");
    };
    assert_eq!(tag.as_ref(), "Cons");
}

#[test]
fn struct_like_single_variant_adt_roundtrip() {
    let mut ts = mk_type_system();
    let heap = Heap::new();

    let mut supply = TypeVarSupply::new();
    let mut foo = AdtDecl::new(&Symbol::intern("Foo"), &[], &mut supply);
    foo.add_variant(
        Symbol::intern("Foo"),
        vec![Type::record(vec![
            (Symbol::intern("a"), Type::builtin(BuiltinTypeId::U64)),
            (Symbol::intern("b"), Type::builtin(BuiltinTypeId::String)),
        ])],
    );
    ts.register_adt(&foo);

    let foo_ty = Type::con("Foo", 0);
    let foo_json = json!({ "a": 42, "b": "Hello" });

    let foo_handle = json_to_rex(&heap, &foo_json, &foo_ty, &ts).unwrap();
    let Value::Adt(tag, args) = foo_handle.value().unwrap() else {
        panic!("expected Foo ADT");
    };
    assert_eq!(tag.as_ref(), "Foo");
    assert_eq!(args.len(), 1);
    assert_eq!(rex_to_json(&foo_handle, &foo_ty, &ts).unwrap(), foo_json);
}

#[test]
fn unit_enum_string_roundtrip() {
    let mut ts = mk_type_system();
    let heap = Heap::new();

    let color = mk_unit_enum("Color", &["Red", "Green", "Blue"]);
    ts.register_adt(&color);
    let color_ty = Type::con("Color", 0);

    for v in [json!("Red"), json!("Green"), json!("Blue")] {
        let handle = json_to_rex(&heap, &v, &color_ty, &ts).unwrap();
        let actual = rex_to_json(&handle, &color_ty, &ts).unwrap();
        assert_eq!(actual, v);
    }
}

#[tokio::test]
async fn eval_entry_points_return_type_for_json_eval() {
    fn engine_with_eval_json_record() -> Engine {
        let mut engine = Engine::with_prelude(()).unwrap();
        EvalJsonRecord::inject_rex(&mut engine).unwrap();
        engine
    }

    let rex_code = "EvalJsonRecord { id = 7, values = to_array [1, 2, 3, 5, 8] }";
    let expected_json = json!({
        "id": 7,
        "values": [1, 2, 3, 5, 8]
    });
    assert_eq!(
        serde_json::to_value(EvalJsonRecord {
            id: 7,
            values: vec![1, 2, 3, 5, 8],
        })
        .unwrap(),
        expected_json
    );
    let expr_program = parse_program(rex_code);
    let engine = engine_with_eval_json_record();
    let type_system = engine.type_system.clone();
    let (handle_eval, ty_eval) = engine
        .into_evaluator()
        .eval(expr_program.body.as_ref().unwrap().as_ref())
        .await
        .unwrap();
    assert_eval_json(&type_system, &handle_eval, &ty_eval, expected_json.clone());
    let (handle_snippet, ty_snippet) = engine_with_eval_json_record()
        .into_evaluator()
        .eval_snippet(rex_code)
        .await
        .unwrap();
    assert_eval_json(
        &type_system,
        &handle_snippet,
        &ty_snippet,
        expected_json.clone(),
    );

    let dir = temp_dir("snippet-at");
    let importer = dir.join("main.rex");
    fs::write(&importer, "()").unwrap();
    let (handle_snippet_at, ty_snippet_at) = engine_with_eval_json_record()
        .into_evaluator()
        .eval_snippet_at(rex_code, &importer)
        .await
        .unwrap();
    assert_eval_json(
        &type_system,
        &handle_snippet_at,
        &ty_snippet_at,
        expected_json,
    );
}
