#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
use std::sync::OnceLock;

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
use bevy::ecs::system::RunSystemOnce as _;
#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
use bevy::prelude::*;
#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
use bevy_scriptum::{FuncArgs, Runtime, prelude::*};

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
static TRACING_SUBSCRIBER: OnceLock<()> = OnceLock::new();

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
#[derive(Default, Resource)]
struct TimesCalled {
    times_called: u8,
}

macro_rules! assert_n_times_called {
    ($app: expr, $count: expr) => {
        assert_eq!(
            $app.world()
                .get_resource::<TimesCalled>()
                .unwrap()
                .times_called,
            $count
        );
    };
}

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
fn build_test_app() -> App {
    let mut app = App::new();

    if std::env::var("RUST_TEST_LOG").is_ok() {
        TRACING_SUBSCRIBER.get_or_init(|| {
            tracing_subscriber::fmt().init();
        });
    }

    app.add_plugins((AssetPlugin::default(), TaskPoolPlugin::default()));
    app
}

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
fn run_script<R: Runtime, Out, Marker>(
    app: &mut App,
    path: String,
    system: impl IntoSystem<(), Out, Marker>,
) -> Entity {
    let asset_server = app.world_mut().get_resource_mut::<AssetServer>().unwrap();
    let asset = asset_server.load::<R::ScriptAsset>(path);

    let entity_id = app.world_mut().spawn(Script::new(asset)).id();
    app.update(); // let `ScriptData` resources be added to entities
    app.world_mut().run_system_once(system).unwrap();
    app.update(); // let callbacks be executed

    entity_id
}

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
fn call_script_on_update_from_rust<R: Runtime>(
    mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
    scripting_runtime: ResMut<R>,
) where
    (): for<'a> FuncArgs<'a, R::Value, R>,
{
    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
    scripting_runtime
        .call_fn("test_func", &mut script_data, entity, ())
        .unwrap();
}

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
trait AssertStateKeyValue {
    type ScriptData;
    fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64);
    fn assert_state_key_value_i32(world: &World, entity_id: Entity, key: &str, value: i32);
    fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str);
}

#[cfg(any(
    feature = "rhai",
    feature = "lua",
    feature = "ruby",
    feature = "javascript"
))]
macro_rules! scripting_tests {
    ($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
        use super::*;

        #[test]
        fn call_script_function_with_params() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/call_script_function_with_params.{}",
                    $script, $extension
                )
                .to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    scripting_runtime
                        .call_fn("test_func", &mut script_data, entity, vec![1])
                        .unwrap();
                },
            );
        }

        #[test]
        fn rust_function_gets_called_from_script_with_param() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct IntResource {
                my_int: i64,
            }

            app.world_mut().init_resource::<IntResource>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((x,)): In<(i64,)>, mut res: ResMut<IntResource>| {
                        res.my_int = x;
                    },
                );
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/rust_function_gets_called_from_script_with_param.{}",
                    $script, $extension
                )
                .to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_eq!(app.world().get_resource::<IntResource>().unwrap().my_int, 5);
        }

        #[test]
        fn rust_function_gets_called_from_script_with_multiple_params() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct TestResource {
                a: i64,
                b: String,
            }

            app.world_mut().init_resource::<TestResource>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((a, b)): In<(i64, String)>, mut res: ResMut<TestResource>| {
                        res.a = a;
                        res.b = b;
                    },
                );
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/rust_function_gets_called_from_script_with_multiple_params.{}",
                    $script, $extension
                )
                .to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_eq!(app.world().get_resource::<TestResource>().unwrap().a, 5);
            assert_eq!(
                app.world().get_resource::<TestResource>().unwrap().b,
                String::from("test")
            );
        }

        #[test]
        fn test_script_function_gets_called_from_rust_with_single_param() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            let entity_id = run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/script_function_gets_called_from_rust_with_single_param.{}",
                    $script, $extension
                )
                .to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    scripting_runtime
                        .call_fn("test_func", &mut script_data, entity, vec![1])
                        .unwrap();
                },
            );

            <$runtime>::assert_state_key_value_i32(&app.world(), entity_id, "a_value", 1i32);
        }

        #[test]
        fn test_script_function_gets_called_from_rust_with_multiple_params() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            let entity_id = run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/script_function_gets_called_from_rust_with_multiple_params.{}",
                    $script, $extension
                )
                .to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    scripting_runtime
                        .call_fn(
                            "test_func",
                            &mut script_data,
                            entity,
                            (1, String::from("abc")),
                        )
                        .unwrap();
                },
            );

            <$runtime>::assert_state_key_value_i32(&app.world(), entity_id, "a_value", 1i32);
            <$runtime>::assert_state_key_value_string(
                &app.world(),
                entity_id,
                "b_value",
                &String::from("abc"),
            );
        }

        #[test]
        fn eval_that_casues_runtime_error_doesnt_panic() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|r| {
                r.add_function(
                    String::from("mark_called"),
                    |mut times_called: ResMut<TimesCalled>| {
                        times_called.times_called += 1;
                    },
                );
            })
            .init_resource::<TimesCalled>();

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/eval_that_causes_runtime_error.{}",
                    $script, $extension
                )
                .to_string(),
                || {},
            );

            assert_n_times_called!(app, 2);
        }

        #[test]
        fn call_script_function_that_casues_runtime_error() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/call_script_function_that_causes_runtime_error.{}",
                    $script, $extension
                )
                .to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    let result =
                        scripting_runtime.call_fn("test_func", &mut script_data, entity, ());
                    assert!(result.is_err());
                },
            );
        }

        #[test]
        fn call_script_function_that_does_not_exist() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/side_effects.{}", $script, $extension).to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    let result =
                        scripting_runtime.call_fn("does_not_exist", &mut script_data, entity, ());
                    assert!(result.is_err());
                },
            );
        }

        #[test]
        fn script_function_gets_called_from_rust() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|_| {});

            let entity_id = run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/script_function_gets_called_from_rust.{}",
                    $script, $extension
                )
                .to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            <$runtime>::assert_state_key_value_i64(&app.world(), entity_id, "times_called", 1i64);
        }

        #[test]
        fn return_via_promise() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(String::from("rust_func"), || 123);
            });

            let entity_id = run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/return_via_promise.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            <$runtime>::assert_state_key_value_i32(&app.world(), entity_id, "x", 123i32);
        }

        #[test]
        fn promise_runtime_error() {
            let mut app = build_test_app();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(String::from("rust_func"), || 123);
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/promise_runtime_error.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );
        }

        #[test]
        fn side_effects() {
            let mut app = build_test_app();

            #[derive(Component)]
            struct MyTag;

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(String::from("spawn_entity"), |mut commands: Commands| {
                    commands.spawn(MyTag);
                });
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/side_effects.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            app.world_mut()
                .run_system_once(|tagged: Query<&MyTag>| {
                    tagged.single().unwrap();
                })
                .unwrap();
        }

        #[test]
        fn rust_function_gets_called_from_script() {
            let mut app = build_test_app();

            app.world_mut().init_resource::<TimesCalled>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(String::from("rust_func"), |mut res: ResMut<TimesCalled>| {
                    res.times_called += 1;
                });
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!(
                    "tests/{}/rust_function_gets_called_from_script.{}",
                    $script, $extension
                )
                .to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_n_times_called!(app, 1);
        }

        #[test]
        fn entity_variable_index_is_available_in_callback() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct State {
                index: u32,
            }

            app.world_mut().init_resource::<State>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((index,)): In<(u32,)>, mut res: ResMut<State>| {
                        res.index = index;
                    },
                );
            });

            let entity = run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/entity_variable.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_eq!(
                app.world().get_resource::<State>().unwrap().index,
                entity.index_u32()
            );
        }

        #[test]
        fn entity_variable_index_is_available_in_eval() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct State {
                index: Option<u32>,
            }

            app.world_mut().init_resource::<State>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((index,)): In<(u32,)>, mut res: ResMut<State>| {
                        res.index = Some(index);
                    },
                );
            });

            let entity = run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/entity_variable_eval.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_eq!(
                app.world().get_resource::<State>().unwrap().index,
                Some(entity.index_u32())
            );
        }

        #[test]
        fn pass_entity_from_script() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct State {
                index: Option<u32>,
            }

            app.world_mut().init_resource::<State>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((entity,)): In<($entity_type,)>, mut res: ResMut<State>| {
                        res.index = Some(entity.index());
                    },
                );
            });

            let entity = run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/pass_entity_from_script.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert_eq!(
                app.world().get_resource::<State>().unwrap().index,
                Some(entity.index_u32())
            );
        }

        #[test]
        fn pass_vec3_from_script() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct State {
                success: bool,
            }

            app.world_mut().init_resource::<State>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(
                    String::from("rust_func"),
                    |In((v,)): In<($vec_type,)>, mut res: ResMut<State>| {
                        assert_eq!(v.x(), 1.5);
                        assert_eq!(v.y(), 2.5);
                        assert_eq!(v.z(), -3.5);
                        res.success = true
                    },
                );
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/pass_vec3_from_script.{}", $script, $extension).to_string(),
                call_script_on_update_from_rust::<$runtime>,
            );

            assert!(app.world().get_resource::<State>().unwrap().success);
        }

        #[test]
        fn pass_vec3_to_script() {
            let mut app = build_test_app();

            #[derive(Default, Resource)]
            struct State {
                success: bool,
            }

            app.world_mut().init_resource::<State>();

            app.add_scripting::<$runtime>(|runtime| {
                runtime.add_function(String::from("mark_success"), |mut res: ResMut<State>| {
                    res.success = true
                });
            });

            run_script::<$runtime, _, _>(
                &mut app,
                format!("tests/{}/pass_vec3_to_script.{}", $script, $extension).to_string(),
                |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
                 scripting_runtime: ResMut<$runtime>| {
                    let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
                    scripting_runtime
                        .call_fn(
                            "test_func",
                            &mut script_data,
                            entity,
                            (<$vec_type>::new(1.5, 2.5, -3.5),),
                        )
                        .unwrap();
                },
            );

            assert!(app.world().get_resource::<State>().unwrap().success);
        }
    };
}

#[cfg(feature = "rhai")]
mod rhai_tests {
    use bevy::prelude::*;
    use bevy_scriptum::runtimes::rhai::prelude::*;

    impl AssertStateKeyValue for RhaiRuntime {
        type ScriptData = RhaiScriptData;

        fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64) {
            let script_data = world.get::<Self::ScriptData>(entity_id).unwrap();
            let state = script_data.scope.get_value::<rhai::Map>("state").unwrap();
            assert_eq!(state[key].clone_cast::<i64>(), value);
        }

        fn assert_state_key_value_i32(world: &World, entity_id: Entity, key: &str, value: i32) {
            let script_data = world.get::<Self::ScriptData>(entity_id).unwrap();
            let state = script_data.scope.get_value::<rhai::Map>("state").unwrap();
            assert_eq!(state[key].clone_cast::<i32>(), value);
        }

        fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str) {
            let script_data = world.get::<Self::ScriptData>(entity_id).unwrap();
            let state = script_data.scope.get_value::<rhai::Map>("state").unwrap();
            assert_eq!(state[key].clone_cast::<String>(), value);
        }
    }

    scripting_tests!(RhaiRuntime, "rhai", "rhai", BevyEntity, BevyVec3);
}

#[cfg(feature = "lua")]
mod lua_tests {
    use bevy::prelude::*;
    use bevy_scriptum::runtimes::lua::prelude::*;
    use mlua::Table;

    impl AssertStateKeyValue for LuaRuntime {
        type ScriptData = LuaScriptData;

        fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
            let runtime = world.get_resource::<LuaRuntime>().unwrap();
            runtime.with_engine(|engine| {
                let state = engine.globals().get::<_, Table>("State").unwrap();
                assert_eq!(state.get::<_, i64>(key).unwrap(), value);
            });
        }

        fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
            let runtime = world.get_resource::<LuaRuntime>().unwrap();
            runtime.with_engine(|engine| {
                let state = engine.globals().get::<_, Table>("State").unwrap();
                assert_eq!(state.get::<_, i32>(key).unwrap(), value);
            });
        }

        fn assert_state_key_value_string(
            world: &World,
            _entity_id: Entity,
            key: &str,
            value: &str,
        ) {
            let runtime = world.get_resource::<LuaRuntime>().unwrap();
            runtime.with_engine(|engine| {
                let state = engine.globals().get::<_, Table>("State").unwrap();
                assert_eq!(state.get::<_, String>(key).unwrap(), value);
            });
        }
    }

    scripting_tests!(LuaRuntime, "lua", "lua", BevyEntity, BevyVec3);
}

#[cfg(feature = "ruby")]
mod ruby_tests {
    use bevy::prelude::*;
    use bevy_scriptum::runtimes::ruby::{RubyScriptData, prelude::*};
    use magnus::value::ReprValue;

    impl AssertStateKeyValue for RubyRuntime {
        type ScriptData = RubyScriptData;

        fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
            let runtime = world.get_resource::<RubyRuntime>().unwrap();
            let key = key.to_string();
            runtime.with_engine_send(move |engine| {
                let state: magnus::value::Value = engine.eval("$state").unwrap();
                let res: i64 = state.funcall_public("[]", (key,)).unwrap();
                assert_eq!(res, value)
            })
        }

        fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
            let runtime = world.get_resource::<RubyRuntime>().unwrap();
            let key = key.to_string();
            runtime.with_engine_send(move |engine| {
                let state: magnus::value::Value = engine.eval("$state").unwrap();
                let res: i32 = state.funcall_public("[]", (key,)).unwrap();
                assert_eq!(res, value)
            })
        }

        fn assert_state_key_value_string(
            world: &World,
            _entity_id: Entity,
            key: &str,
            value: &str,
        ) {
            let runtime = world.get_resource::<RubyRuntime>().unwrap();
            let key = key.to_string();
            let value = value.to_string();
            runtime.with_engine_send(move |engine| {
                let state: magnus::value::Value = engine.eval("$state").unwrap();
                let res: String = state.funcall_public("[]", (key,)).unwrap();
                assert_eq!(res, value);
            });
        }
    }

    #[test]
    fn test_symbol_inspection() {
        let mut app = build_test_app();

        app.add_scripting::<RubyRuntime>(|_| {});
        let runtime = app.world().get_resource::<RubyRuntime>().unwrap();
        runtime.with_engine_send(|engine| {
            let symbol_string: String = engine.eval(":test_symbol.inspect").unwrap();
            assert_eq!(symbol_string, ":test_symbol")
        });
    }

    scripting_tests!(RubyRuntime, "ruby", "rb", BevyEntity, BevyVec3);
}

#[cfg(feature = "javascript")]
mod javascript_tests {
    use bevy::prelude::*;
    use bevy_scriptum::runtimes::javascript::{prelude::*, v8};

    fn with_state_value<T: Send + 'static>(
        world: &World,
        key: &str,
        f: impl FnOnce(&mut v8::PinScope, v8::Local<v8::Value>) -> T + Send + 'static,
    ) -> T {
        let runtime = world.get_resource::<JsRuntime>().unwrap();
        let key = key.to_string();
        runtime.with_scope(move |scope| {
            let global = scope.get_current_context().global(scope);
            let state_key: v8::Local<v8::Value> = v8::String::new(scope, "State").unwrap().into();
            let state = global
                .get(scope, state_key)
                .unwrap()
                .to_object(scope)
                .unwrap();
            let value_key: v8::Local<v8::Value> = v8::String::new(scope, &key).unwrap().into();
            let value = state.get(scope, value_key).unwrap();
            f(scope, value)
        })
    }

    impl AssertStateKeyValue for JsRuntime {
        type ScriptData = JsScriptData;

        fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
            let actual = with_state_value(world, key, |scope, val| {
                val.number_value(scope).unwrap() as i64
            });
            assert_eq!(actual, value);
        }

        fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
            let actual = with_state_value(world, key, |scope, val| {
                val.number_value(scope).unwrap() as i32
            });
            assert_eq!(actual, value);
        }

        fn assert_state_key_value_string(
            world: &World,
            _entity_id: Entity,
            key: &str,
            value: &str,
        ) {
            let actual = with_state_value(world, key, |scope, val| val.to_rust_string_lossy(scope));
            assert_eq!(actual, value);
        }
    }

    scripting_tests!(JsRuntime, "js", "js", BevyEntity, BevyVec3);
}
