Documentation
use bevy_app::App;
use bevy_ecs::component::Component;
use bevy_ecs::prelude::{AppTypeRegistry, ReflectComponent};
use bevy_ecs::reflect::AppFunctionRegistry;
use bevy_reflect::{Reflect, TypePath};

use wasvy::WasvyComponent;
use wasvy::methods::MethodTarget;
use wasvy::prelude::*;
use wasvy::serialize::CodecResource;

#[derive(Component, Reflect, Default, WasvyComponent)]
#[reflect(Component)]
struct Health {
    current: f32,
    max: f32,
}

#[wasvy::methods]
impl Health {
    fn heal(&mut self, amount: f32) {
        self.current = (self.current + amount).min(self.max);
    }

    fn pct(&self) -> f32 {
        self.current / self.max
    }

    #[wasvy::skip]
    #[allow(dead_code)]
    fn internal_ratio(&self) -> f32 {
        self.current / self.max
    }
}

fn new_app() -> App {
    let mut app = App::new();
    app.add_plugins(WasvyAutoRegistrationPlugin);
    app.insert_resource(CodecResource::default());
    app
}

#[test]
fn methods_macro_registers() {
    let app = new_app();

    let mut health = Health {
        current: 2.0,
        max: 10.0,
    };

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);
    let out = index
        .invoke(
            Health::type_path(),
            "heal",
            MethodTarget::Write(&mut health),
            b"[5.0]",
            type_registry,
            codec,
        )
        .unwrap();

    assert_eq!(out, b"null");
    assert_eq!(health.current, 7.0);

    let pct = index
        .invoke(
            Health::type_path(),
            "pct",
            MethodTarget::Read(&health),
            b"null",
            type_registry,
            codec,
        )
        .unwrap();

    let pct_val: f32 = wasvy::serialize::wasvy_decode(&pct).unwrap();
    assert!((pct_val - 0.7).abs() < 1e-6);
}

#[test]
fn component_plugin_registers_type() {
    let mut app = App::new();
    app.add_plugins(WasvyComponentPlugin::<Health>::default());

    let registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let registry = registry.read();

    assert!(registry.get_with_type_path(Health::type_path()).is_some());
    let registration = registry
        .get_with_type_path(Health::type_path())
        .expect("type registration");
    assert!(
        registration
            .data::<wasvy::authoring::WasvyExport>()
            .is_some()
    );
}

#[test]
fn auto_registration_plugin_registers_all() {
    let app = new_app();

    {
        let registry = app
            .world()
            .get_resource::<AppTypeRegistry>()
            .expect("AppTypeRegistry to exist");
        let registry = registry.read();
        assert!(registry.get_with_type_path(Health::type_path()).is_some());
    }

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);
    let mut health = Health {
        current: 2.0,
        max: 10.0,
    };
    let out = index
        .invoke(
            Health::type_path(),
            "heal",
            MethodTarget::Write(&mut health),
            b"[1.0]",
            type_registry,
            codec,
        )
        .unwrap();
    assert_eq!(out, b"null");
    assert!((health.current - 3.0).abs() < f32::EPSILON);
}

#[test]
fn skip_attribute_excludes_method() {
    let app = new_app();

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);

    let health = Health {
        current: 2.0,
        max: 10.0,
    };

    let err = index
        .invoke(
            Health::type_path(),
            "internal_ratio",
            MethodTarget::Read(&health),
            b"null",
            type_registry,
            codec,
        )
        .unwrap_err();

    assert!(
        err.to_string().contains("internal_ratio"),
        "unexpected error: {err}"
    );
}

#[test]
fn wit_uses_arg_names() {
    let app = new_app();

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let settings = wasvy::witgen::WitGeneratorSettings::default();
    let output = wasvy::witgen::generate_wit(&settings, type_registry, function_registry);

    assert!(output.contains("heal: func(amount: f32)"), "{output}");
}

#[test]
fn invoke_errors_on_missing_method() {
    let app = new_app();

    let mut health = Health {
        current: 1.0,
        max: 2.0,
    };

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);

    let err = index
        .invoke(
            Health::type_path(),
            "missing",
            MethodTarget::Write(&mut health),
            b"[]",
            type_registry,
            codec,
        )
        .unwrap_err();

    assert!(err.to_string().contains("Unknown method"));
}

#[test]
fn invoke_errors_on_wrong_access() {
    let app = new_app();

    let health = Health {
        current: 1.0,
        max: 2.0,
    };

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);

    let err = index
        .invoke(
            Health::type_path(),
            "heal",
            MethodTarget::Read(&health),
            b"[1.0]",
            type_registry,
            codec,
        )
        .unwrap_err();

    assert!(err.to_string().contains("requires mutable access"));
}

#[test]
fn invoke_errors_on_arg_count_mismatch() {
    let app = new_app();

    let mut health = Health {
        current: 1.0,
        max: 2.0,
    };

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);

    let err = index
        .invoke(
            Health::type_path(),
            "heal",
            MethodTarget::Write(&mut health),
            b"[]",
            type_registry,
            codec,
        )
        .unwrap_err();

    assert!(err.to_string().contains("expects"));
}

#[test]
fn invoke_errors_on_bad_json() {
    let app = new_app();

    let mut health = Health {
        current: 1.0,
        max: 2.0,
    };

    let type_registry = app
        .world()
        .get_resource::<AppTypeRegistry>()
        .expect("AppTypeRegistry to exist");
    let codec = app
        .world()
        .get_resource::<CodecResource>()
        .expect("CodecResource to exist");
    let function_registry = app
        .world()
        .get_resource::<AppFunctionRegistry>()
        .expect("AppFunctionRegistry to exist");
    let index = FunctionIndex::build(type_registry, function_registry);

    let err = index
        .invoke(
            Health::type_path(),
            "heal",
            MethodTarget::Write(&mut health),
            b"[",
            type_registry,
            codec,
        )
        .unwrap_err();

    let message = err.to_string().to_lowercase();
    assert!(
        message.contains("parse") || message.contains("eof") || message.contains("json"),
        "unexpected error: {err}"
    );
}