use mocari::core::{
    PhysicsInputAccumulator, PhysicsParticle, PhysicsRange, Vector2, direction_to_radian,
    normalize_physics_parameter, parent_gravity_for_physics_output,
    physics_output_angle_with_parent_gravity, physics_output_translation_x,
    physics_output_translation_y, radian_to_direction, stabilize_physics_particles,
    update_physics_particles,
};

fn assert_close(actual: f32, expected: f32) {
    assert!(
        (actual - expected).abs() < 0.00001,
        "expected {expected}, got {actual}"
    );
}

#[test]
fn physics_parameter_normalization_uses_range_midpoint() {
    let parameter = PhysicsRange::new(0.0, 10.0, 2.0);
    let normalized = PhysicsRange::new(0.0, 1.0, 0.5);

    assert_close(
        normalize_physics_parameter(7.5, parameter, normalized, true),
        0.75,
    );
    assert_close(
        normalize_physics_parameter(7.5, parameter, normalized, false),
        -0.75,
    );
}

#[test]
fn physics_input_accumulator_applies_weighted_channels() {
    let parameter = PhysicsRange::new(-30.0, 30.0, 0.0);
    let position = PhysicsRange::new(-10.0, 10.0, 0.0);
    let angle = PhysicsRange::new(-30.0, 30.0, 0.0);
    let mut input = PhysicsInputAccumulator::default();

    input.add_translation_x(30.0, parameter, position, true, 50.0);
    input.add_translation_y(-30.0, parameter, position, false, 25.0);
    input.add_angle(15.0, parameter, angle, true, 100.0);

    assert_close(input.translation_x(), 5.0);
    assert_close(input.translation_y(), 2.5);
    assert_close(input.angle(), 15.0);
}

#[test]
fn physics_output_translation_reflects_axes() {
    let translation = Vector2::new(3.0, -4.0);

    assert_close(physics_output_translation_x(translation, false), 3.0);
    assert_close(physics_output_translation_x(translation, true), -3.0);
    assert_close(physics_output_translation_y(translation, false), -4.0);
    assert_close(physics_output_translation_y(translation, true), 4.0);
}

#[test]
fn physics_output_angle_uses_direction_delta() {
    let parent_gravity = Vector2::new(0.0, 1.0);
    let translation = Vector2::new(1.0, 0.0);

    assert_close(
        direction_to_radian(parent_gravity, translation),
        -std::f32::consts::FRAC_PI_2,
    );
    assert_eq!(radian_to_direction(0.0), parent_gravity);
    assert_close(
        physics_output_angle_with_parent_gravity(translation, parent_gravity, false),
        -std::f32::consts::FRAC_PI_2,
    );
    assert_close(
        physics_output_angle_with_parent_gravity(translation, parent_gravity, true),
        std::f32::consts::FRAC_PI_2,
    );
}

#[test]
fn physics_output_parent_gravity_matches_particle_index_branch() {
    let particles = [
        Vector2::new(1.0, 2.0),
        Vector2::new(4.0, 6.0),
        Vector2::new(10.0, 6.0),
    ];
    let parent_gravity = Vector2::new(0.0, -1.0);

    assert_eq!(
        parent_gravity_for_physics_output(&particles, 0, parent_gravity),
        Some(Vector2::new(0.0, 1.0))
    );
    assert_eq!(
        parent_gravity_for_physics_output(&particles, 1, parent_gravity),
        Some(Vector2::new(0.0, 1.0))
    );
    assert_eq!(
        parent_gravity_for_physics_output(&particles, 2, parent_gravity),
        Some(Vector2::new(3.0, 4.0))
    );
    assert_eq!(
        parent_gravity_for_physics_output(&particles, 3, parent_gravity),
        Some(Vector2::new(6.0, 0.0))
    );
    assert_eq!(
        parent_gravity_for_physics_output(&particles, 4, parent_gravity),
        None
    );
}

#[test]
fn physics_particles_update_positions_and_velocity() {
    let mut particles = [
        PhysicsParticle::new(
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 1.0),
            1.0,
            1.0,
            0.0,
            0.0,
        ),
        PhysicsParticle::new(
            Vector2::new(0.0, 1.0),
            Vector2::new(0.0, 1.0),
            Vector2::new(1.0, 0.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 1.0),
            1.0,
            1.0,
            0.0,
            1.0,
        ),
    ];

    update_physics_particles(
        &mut particles,
        Vector2::new(0.0, 0.0),
        0.0,
        Vector2::new(0.0, 0.0),
        0.001,
        1.0 / 30.0,
        5.0,
    );

    assert_close(particles[1].position().x(), std::f32::consts::FRAC_1_SQRT_2);
    assert_close(particles[1].position().y(), std::f32::consts::FRAC_1_SQRT_2);
    assert_close(particles[1].velocity().x(), std::f32::consts::FRAC_1_SQRT_2);
    assert_close(
        particles[1].velocity().y(),
        std::f32::consts::FRAC_1_SQRT_2 - 1.0,
    );
}

#[test]
fn physics_particles_stabilize_along_force_direction() {
    let mut particles = [
        PhysicsParticle::new(
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(3.0, 4.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 1.0),
            1.0,
            1.0,
            0.0,
            0.0,
        ),
        PhysicsParticle::new(
            Vector2::new(10.0, 10.0),
            Vector2::new(10.0, 10.0),
            Vector2::new(3.0, 4.0),
            Vector2::new(0.0, 0.0),
            Vector2::new(0.0, 1.0),
            1.0,
            1.0,
            2.0,
            5.0,
        ),
    ];

    stabilize_physics_particles(
        &mut particles,
        Vector2::new(2.0, 3.0),
        0.0,
        Vector2::new(0.0, 0.0),
        0.001,
    );

    assert_eq!(particles[0].position(), Vector2::new(2.0, 3.0));
    assert_eq!(particles[1].position(), Vector2::new(2.0, 8.0));
    assert_eq!(particles[1].velocity(), Vector2::new(0.0, 0.0));
}
