// Tests for API improvement proposals (P1–P11)
use crate::*;

// ============================
// P1: call / call_global
// ============================

#[test]
fn test_call_lua_function() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("function add(a, b) return a + b end").unwrap();
    let func = vm.get_global("add").unwrap().unwrap();
    let result: i64 = vm.call1(func, (3, 4)).unwrap();
    assert_eq!(result, 7);
}

#[test]
fn test_call_lua_function_raw() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("function add(a, b) return a + b end").unwrap();
    let func = vm.get_global("add").unwrap().unwrap();
    let results = vm
        .call_raw(func, vec![LuaValue::integer(3), LuaValue::integer(4)])
        .unwrap();
    assert_eq!(results[0].as_integer(), Some(7));
}

#[test]
fn test_call_global() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("function greet(name) return 'Hello, ' .. name end")
        .unwrap();
    let result: String = vm.call1_global("greet", "World").unwrap();
    assert_eq!(result, "Hello, World");
}

#[test]
fn test_call_global_not_found() {
    let mut vm = LuaVM::new(SafeOption::default());
    let result: LuaResult<Vec<LuaValue>> = vm.call_global("nonexistent", ());
    assert!(result.is_err());
}

// ============================
// P2: register_function
// ============================

#[test]
fn test_register_function() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.register_function("rust_add", |state| {
        let a = state.get_arg(1).and_then(|v| v.as_integer()).unwrap_or(0);
        let b = state.get_arg(2).and_then(|v| v.as_integer()).unwrap_or(0);
        state.push_value(LuaValue::integer(a + b))?;
        Ok(1)
    })
    .unwrap();

    let results = vm.execute("return rust_add(10, 20)").unwrap();
    assert_eq!(results[0].as_integer(), Some(30));
}

#[test]
fn test_register_function_typed() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.register_function_typed("rust_add_typed", |a: i64, b: i64| a + b)
        .unwrap();

    let results = vm.execute("return rust_add_typed(10, 20)").unwrap();
    assert_eq!(results[0].as_integer(), Some(30));
}

#[test]
fn test_register_function_typed_userdata_ref() {
    #[derive(Debug)]
    struct Counter {
        count: i64,
    }

    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let counter = vm.push_any(Counter { count: 1 }).unwrap();
    vm.set_global("counter", counter).unwrap();

    vm.register_function_typed(
        "increment_typed",
        |mut counter: UserDataRef<Counter>, delta: i64| {
            let counter_ref = counter.get_mut().unwrap();
            counter_ref.count += delta;
            counter_ref.count
        },
    )
    .unwrap();

    let results = vm.execute("return increment_typed(counter, 9)").unwrap();
    assert_eq!(results[0].as_integer(), Some(10));

    let counter: UserDataRef<Counter> = vm.get_global_as("counter").unwrap().unwrap();
    assert_eq!(counter.get().unwrap().count, 10);
}

#[test]
fn test_register_function_typed_high_arity() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.register_function_typed(
        "sum8",
        |a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64, h: i64| {
            a + b + c + d + e + f + g + h
        },
    )
    .unwrap();

    let results = vm.execute("return sum8(1, 2, 3, 4, 5, 6, 7, 8)").unwrap();
    assert_eq!(results[0].as_integer(), Some(36));
}

// ============================
// P3: load / load_with_name
// ============================

#[test]
fn test_load_and_call() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let func = vm.load("return 42").unwrap();
    let result: i64 = vm.call1(func, ()).unwrap();
    assert_eq!(result, 42);
}

#[test]
fn test_load_with_name() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let func = vm.load_with_name("return 'hello'", "@my_script").unwrap();
    let result: String = vm.call1(func, ()).unwrap();
    assert_eq!(result, "hello");
}

#[test]
fn test_load_does_not_execute() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    // Load but don't execute — global should not be set
    let _func = vm.load("x = 999").unwrap();
    let x = vm.get_global("x").unwrap();
    assert!(x.is_none());
}

#[cfg(feature = "shared-proto")]
#[test]
fn test_load_marks_chunk_short_strings_shared() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let func = vm
        .load("local key = '0123456789abcdefghijklmnopqr'; return key")
        .unwrap();

    let function = func.as_function_ptr().unwrap();
    let chunk = function.as_ref().data.chunk();
    let key = chunk
        .constants
        .iter()
        .copied()
        .find(|value| value.as_str() == Some("0123456789abcdefghijklmnopqr"))
        .unwrap();

    assert!(key.is_short_string());
    assert!(key.as_string_ptr().unwrap().as_ref().header.is_shared());
    assert!(function.as_ref().data.proto().as_ref().header.is_shared());
}

#[cfg(feature = "shared-proto")]
#[test]
fn test_shared_proto_survives_vm_drop() {
    let proto = {
        let mut vm = LuaVM::new(SafeOption::default());
        vm.open_stdlib(Stdlib::All).unwrap();

        let func = vm
            .load("local key = '0123456789abcdefghijklmnopqr'; return key")
            .unwrap();

        func.as_function_ptr().unwrap().as_ref().data.proto()
    };

    assert!(proto.as_ref().header.is_shared());

    let key = proto
        .as_ref()
        .data
        .constants
        .iter()
        .copied()
        .find(|value| value.as_str() == Some("0123456789abcdefghijklmnopqr"))
        .unwrap();

    assert!(key.as_string_ptr().unwrap().as_ref().header.is_shared());
}

#[cfg(feature = "shared-proto")]
#[test]
fn test_shared_proto_reuses_same_file_across_vms() {
    use std::io::Write;

    let path = std::env::temp_dir().join("lua_rs_shared_proto_cache.lua");
    {
        let mut file = std::fs::File::create(&path).unwrap();
        writeln!(file, "return 'shared-proto-cache'").unwrap();
    }

    let proto1 = {
        let mut vm = LuaVM::new(SafeOption::default());
        vm.open_stdlib(Stdlib::All).unwrap();
        vm.load_proto_from_file(path.to_str().unwrap()).unwrap()
    };

    let proto2 = {
        let mut vm = LuaVM::new(SafeOption::default());
        vm.open_stdlib(Stdlib::All).unwrap();
        vm.load_proto_from_file(path.to_str().unwrap()).unwrap()
    };

    assert_eq!(proto1, proto2);

    std::fs::remove_file(&path).ok();
}

#[cfg(feature = "shared-proto")]
#[test]
fn test_shared_proto_reloads_when_file_changes() {
    use std::io::Write;

    let path = std::env::temp_dir().join("lua_rs_shared_proto_reload.lua");
    {
        let mut file = std::fs::File::create(&path).unwrap();
        writeln!(file, "return 1").unwrap();
    }

    let proto1 = {
        let mut vm = LuaVM::new(SafeOption::default());
        vm.open_stdlib(Stdlib::All).unwrap();
        vm.load_proto_from_file(path.to_str().unwrap()).unwrap()
    };

    {
        let mut file = std::fs::File::create(&path).unwrap();
        writeln!(file, "return 123456789").unwrap();
    }

    let proto2 = {
        let mut vm = LuaVM::new(SafeOption::default());
        vm.open_stdlib(Stdlib::All).unwrap();
        vm.load_proto_from_file(path.to_str().unwrap()).unwrap()
    };

    assert_ne!(proto1, proto2);

    std::fs::remove_file(&path).ok();
}

// ============================
// P4: register_type_of on LuaVM (tested implicitly via existing userdata tests)
// ============================

// ============================
// P5: TableBuilder
// ============================

#[test]
fn test_table_builder_named_keys() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let host = vm.create_string("localhost").unwrap();
    let table = TableBuilder::new()
        .set("host", host)
        .set("port", LuaValue::integer(8080))
        .build(&mut vm)
        .unwrap();

    let host_key = vm.create_string("host").unwrap();
    let port_key = vm.create_string("port").unwrap();
    assert_eq!(
        vm.raw_get(&table, &host_key).unwrap().as_str(),
        Some("localhost")
    );
    assert_eq!(
        vm.raw_get(&table, &port_key).unwrap().as_integer(),
        Some(8080)
    );
}

#[test]
fn test_table_builder_array() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let table = TableBuilder::new()
        .push(LuaValue::integer(10))
        .push(LuaValue::integer(20))
        .push(LuaValue::integer(30))
        .build(&mut vm)
        .unwrap();

    vm.set_global("arr", table).unwrap();
    let results = vm.execute("return #arr, arr[1], arr[2], arr[3]").unwrap();
    assert_eq!(results[0].as_integer(), Some(3));
    assert_eq!(results[1].as_integer(), Some(10));
    assert_eq!(results[2].as_integer(), Some(20));
    assert_eq!(results[3].as_integer(), Some(30));
}

#[test]
fn test_table_builder_mixed() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let name = vm.create_string("test").unwrap();
    let table = TableBuilder::new()
        .push(LuaValue::integer(1))
        .push(LuaValue::integer(2))
        .set("name", name)
        .build(&mut vm)
        .unwrap();

    vm.set_global("t", table).unwrap();
    let results = vm.execute("return t[1], t[2], t.name").unwrap();
    assert_eq!(results[0].as_integer(), Some(1));
    assert_eq!(results[1].as_integer(), Some(2));
    assert_eq!(results[2].as_str(), Some("test"));
}

// ============================
// P6: LuaError Display / std::error::Error
// ============================

#[test]
fn test_lua_error_display() {
    let err = lua_vm::LuaError::RuntimeError;
    assert_eq!(format!("{}", err), "Runtime Error");
}

#[test]
fn test_lua_error_is_std_error() {
    fn assert_error<E: std::error::Error>(_: &E) {}
    let err = lua_vm::LuaError::RuntimeError;
    assert_error(&err);
}

#[test]
fn test_lua_full_error() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    match vm.execute("error('boom')") {
        Err(e) => {
            let full = vm.into_full_error(e);
            assert_eq!(full.kind(), lua_vm::LuaError::RuntimeError);
            assert!(
                full.message().contains("boom"),
                "message should contain 'boom': {}",
                full
            );
            // Display should work
            let display = format!("{}", full);
            assert!(display.contains("boom"));
        }
        Ok(_) => panic!("expected error"),
    }
}

#[test]
fn test_lua_full_error_is_std_error() {
    fn assert_error<E: std::error::Error>(_: &E) {}
    let full = lua_vm::lua_error::LuaFullError {
        kind: lua_vm::LuaError::CompileError,
        message: "syntax error".to_string(),
    };
    assert_error(&full);
}

// ============================
// P7: Typed global getters
// ============================

#[test]
fn test_get_global_as_integer() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.execute("x = 42").unwrap();
    let x: i64 = vm.get_global_as::<i64>("x").unwrap().unwrap();
    assert_eq!(x, 42);
}

#[test]
fn test_get_global_as_string() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();
    vm.execute("name = 'Alice'").unwrap();
    let name: String = vm.get_global_as::<String>("name").unwrap().unwrap();
    assert_eq!(name, "Alice");
}

#[test]
fn test_get_global_as_bool() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.execute("flag = true").unwrap();
    let flag: bool = vm.get_global_as::<bool>("flag").unwrap().unwrap();
    assert!(flag);
}

#[test]
fn test_get_global_as_none() {
    let mut vm = LuaVM::new(SafeOption::default());
    let result = vm.get_global_as::<i64>("nonexistent").unwrap();
    assert!(result.is_none());
}

// ============================
// P8: open_stdlibs
// ============================

#[test]
fn test_open_stdlibs() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlibs(&[Stdlib::Math, Stdlib::String, Stdlib::Table])
        .unwrap();

    // Math should work
    let results = vm.execute("return math.abs(-5)").unwrap();
    assert_eq!(results[0].as_integer(), Some(5));

    // String should work
    let results = vm.execute("return string.upper('hello')").unwrap();
    assert_eq!(results[0].as_str(), Some("HELLO"));
}

// ============================
// P10: table_pairs / table_length
// ============================

#[test]
fn test_table_pairs() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let table = TableBuilder::new()
        .set("a", LuaValue::integer(1))
        .set("b", LuaValue::integer(2))
        .build(&mut vm)
        .unwrap();

    let pairs = vm.table_pairs(&table).unwrap();
    assert_eq!(pairs.len(), 2);

    // Check that both entries exist (order is not guaranteed)
    let keys: Vec<_> = pairs.iter().map(|(k, _)| k.as_str().unwrap()).collect();
    assert!(keys.contains(&"a"));
    assert!(keys.contains(&"b"));
}

#[test]
fn test_table_length() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let table = TableBuilder::new()
        .push(LuaValue::integer(10))
        .push(LuaValue::integer(20))
        .push(LuaValue::integer(30))
        .build(&mut vm)
        .unwrap();

    let len = vm.table_length(&table).unwrap();
    assert_eq!(len, 3);
}

// ============================
// P11: dofile (tested with a temp file)
// ============================

#[test]
fn test_dofile() {
    use std::io::Write;

    let dir = std::env::temp_dir();
    let path = dir.join("lua_rs_test_dofile.lua");
    {
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(f, "return 1 + 2").unwrap();
    }

    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();
    let results = vm.dofile(path.to_str().unwrap()).unwrap();
    assert_eq!(results[0].as_integer(), Some(3));

    std::fs::remove_file(&path).ok();
}

#[test]
fn test_dofile_not_found() {
    let mut vm = LuaVM::new(SafeOption::default());
    let result = vm.dofile("nonexistent_file_12345.lua");
    assert!(result.is_err());
}

// ============================
// LuaState proxy tests
// ============================

#[test]
fn test_lua_state_load_proxy() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    // register_function that uses LuaState's load proxy
    vm.register_function("test_load", |state| {
        let func = state.load("return 77")?;
        state.push_value(func)?;
        Ok(1)
    })
    .unwrap();

    let results = vm.execute("local f = test_load(); return f()").unwrap();
    assert_eq!(results[0].as_integer(), Some(77));
}

#[test]
fn test_lua_state_call_global_proxy() {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("function double(x) return x * 2 end").unwrap();

    vm.register_function("test_call_global", |state| {
        let results = state.call_global("double", vec![LuaValue::integer(21)])?;
        state.push_value(results[0])?;
        Ok(1)
    })
    .unwrap();

    let results = vm.execute("return test_call_global()").unwrap();
    assert_eq!(results[0].as_integer(), Some(42));
}

// ========== Error Recovery Tests ==========

#[test]
fn test_execute_error_recovery() {
    // After a runtime error in execute(), the VM should remain usable.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    // Cause a runtime error
    let err = vm.execute("error('boom')");
    assert!(err.is_err());

    // VM should still be usable — execute more code
    let results = vm.execute("return 1 + 2").unwrap();
    assert_eq!(results[0].as_integer(), Some(3));
}

#[test]
fn test_execute_error_preserves_globals() {
    // Globals set before an error should still be accessible after recovery.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("x = 42").unwrap();

    let err = vm.execute("error('fail')");
    assert!(err.is_err());

    let results = vm.execute("return x").unwrap();
    assert_eq!(results[0].as_integer(), Some(42));
}

#[test]
fn test_call_global_error_recovery() {
    // call_global error should not corrupt the VM state.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute("function bad() error('nope') end").unwrap();
    vm.execute("function good() return 99 end").unwrap();

    let err: LuaResult<Vec<LuaValue>> = vm.call_global("bad", ());
    assert!(err.is_err());

    // Should still work after the error
    let results: Vec<LuaValue> = vm.call_global("good", ()).unwrap();
    assert_eq!(results[0].as_integer(), Some(99));
}

#[test]
fn test_multiple_errors_recovery() {
    // Multiple consecutive errors should all recover cleanly.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    for i in 0..5 {
        let err = vm.execute(&format!("error('error {}')", i));
        assert!(err.is_err());
    }

    // VM should still work after many errors
    let results = vm.execute("return 'still alive'").unwrap();
    assert_eq!(results[0].as_str(), Some("still alive"));
}

#[test]
fn test_error_message_available_after_recovery() {
    // get_error_message should return the correct message after recovery.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    let err = vm.execute("error('specific error message')").unwrap_err();
    let msg = vm.get_error_message(err);
    assert!(
        msg.contains("specific error message"),
        "expected message to contain 'specific error message', got: {}",
        msg
    );

    // VM should be usable after getting the message
    let results = vm.execute("return true").unwrap();
    assert_eq!(results[0].as_boolean(), Some(true));
}

#[test]
fn test_deep_call_error_recovery() {
    // Error in deeply nested calls should clean up all frames.
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All).unwrap();

    vm.execute(
        r#"
        function a() return b() end
        function b() return c() end
        function c() error('deep error') end
    "#,
    )
    .unwrap();

    let err: LuaResult<Vec<LuaValue>> = vm.call_global("a", ());
    assert!(err.is_err());

    // After deep error, simple calls should work
    let results = vm.execute("return 1 + 1").unwrap();
    assert_eq!(results[0].as_integer(), Some(2));

    // And function calls too
    vm.execute("function simple() return 42 end").unwrap();
    let results: Vec<LuaValue> = vm.call_global("simple", ()).unwrap();
    assert_eq!(results[0].as_integer(), Some(42));
}
