use mocari::{
    assets::{load_model, load_model_runtime},
    expression::{ExpressionManager, ExpressionPlayer, load_expression},
    json::{Expression3, Motion3},
    motion::MotionPlayer,
};

#[test]
fn runtime_default_pose_matches_default_model() {
    let runtime = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let default = load_model("assets/models/Haru/Haru.model3.json").unwrap();

    let runtime_meshes = runtime.runtime().meshes();
    let default_meshes = default.meshes();

    // Geometry must match; opacity may differ because the runtime applies the
    // pose3 part groups (hiding the redundant arm) while load_model does not.
    assert_eq!(runtime_meshes.len(), default_meshes.len());
    for (left, right) in runtime_meshes.iter().zip(default_meshes) {
        assert_eq!(left.vertices(), right.vertices());
    }
}

#[test]
fn setting_a_parameter_changes_mesh_vertices() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let before: Vec<_> = model
        .runtime()
        .meshes()
        .iter()
        .map(|mesh| mesh.vertices().to_vec())
        .collect();

    let angle_x = model
        .runtime()
        .parameter_ids()
        .iter()
        .find(|id| id.as_str() == "ParamAngleX")
        .cloned()
        .expect("Haru has ParamAngleX");
    model.runtime_mut().set_parameter(&angle_x, 30.0);
    model.runtime_mut().update_meshes().unwrap();

    let after: Vec<_> = model
        .runtime()
        .meshes()
        .iter()
        .map(|mesh| mesh.vertices().to_vec())
        .collect();

    assert_ne!(before, after, "moving ParamAngleX should deform the mesh");
}

#[test]
fn updating_parameters_reuses_runtime_mesh_storage() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let mesh_ptr = model.runtime().meshes().as_ptr();
    let vertex_ptrs: Vec<_> = model
        .runtime()
        .meshes()
        .iter()
        .map(|mesh| mesh.vertices().as_ptr())
        .collect();

    assert!(model.runtime_mut().set_parameter("ParamAngleX", 30.0));
    model.runtime_mut().update_meshes().unwrap();

    assert_eq!(model.runtime().meshes().as_ptr(), mesh_ptr);
    assert_eq!(
        model
            .runtime()
            .meshes()
            .iter()
            .map(|mesh| mesh.vertices().as_ptr())
            .collect::<Vec<_>>(),
        vertex_ptrs
    );
}

#[test]
fn set_parameter_clamps_to_model_range() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let id = "ParamAngleX";
    let index = model.runtime().parameter_index(id).unwrap();

    model.runtime_mut().set_parameter(id, 1_000_000.0);
    let high = model.runtime().parameter_value_by_index(index).unwrap();
    model.runtime_mut().set_parameter(id, -1_000_000.0);
    let low = model.runtime().parameter_value_by_index(index).unwrap();

    assert!(high < 1_000_000.0, "value must be clamped to the maximum");
    assert!(low > -1_000_000.0, "value must be clamped to the minimum");
    assert!(low < high);
}

#[test]
fn parameter_info_exposes_range_default_and_current_value() {
    let model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let index = model.runtime().parameter_index("ParamAngleX").unwrap();
    let info = model.runtime().parameter_info_by_index(index).unwrap();

    assert_eq!(info.id(), "ParamAngleX");
    assert!(info.minimum() < info.maximum());
    assert!(info.minimum() <= info.default());
    assert!(info.default() <= info.maximum());
    assert_eq!(info.default(), info.value());
    assert_eq!(
        model.runtime().parameter_infos().count(),
        model.runtime().parameter_ids().len()
    );
}

#[test]
fn set_parameter_normalized_maps_unit_range_to_model_range() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let index = model.runtime().parameter_index("ParamAngleX").unwrap();
    let info = model.runtime().parameter_info_by_index(index).unwrap();
    let expected = info.minimum() + (info.maximum() - info.minimum()) * 0.75;

    assert!(
        model
            .runtime_mut()
            .set_parameter_normalized_by_index(index, 0.75)
    );

    assert_close(
        model.runtime().parameter_value_by_index(index).unwrap(),
        expected,
    );
    assert_close(
        model
            .runtime()
            .parameter_normalized_value_by_index(index)
            .unwrap(),
        0.75,
    );

    assert!(
        model
            .runtime_mut()
            .set_parameter_normalized("ParamAngleX", 2.0)
    );
    assert_close(
        model
            .runtime()
            .parameter_normalized_value_by_index(index)
            .unwrap(),
        1.0,
    );
}

#[test]
fn parameter_overrides_can_be_applied_after_parameter_reset() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let index = model.runtime().parameter_index("ParamAngleX").unwrap();
    let info = model.runtime().parameter_info_by_index(index).unwrap();
    let maximum = info.maximum();
    let default = info.default();

    assert!(
        model
            .runtime_mut()
            .set_parameter_override_normalized_by_index(index, 1.0)
    );
    model.runtime_mut().reset_parameters();
    model.runtime_mut().apply_parameter_overrides();
    assert_close(
        model.runtime().parameter_value_by_index(index).unwrap(),
        maximum,
    );
    assert_close(
        model
            .runtime()
            .parameter_override_normalized_value_by_index(index)
            .unwrap(),
        1.0,
    );

    assert!(model.runtime_mut().clear_parameter_override_by_index(index));
    model.runtime_mut().reset_parameters();
    model.runtime_mut().apply_parameter_overrides();
    assert_close(
        model.runtime().parameter_value_by_index(index).unwrap(),
        default,
    );
    assert!(
        model
            .runtime()
            .parameter_override_value_by_index(index)
            .is_none()
    );
}

#[test]
fn motion_player_drives_a_parameter_over_time() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let motion =
        mocari::motion::load_motion("assets/models/Haru/motions/haru_g_idle.motion3.json").unwrap();

    let target = motion
        .curves()
        .iter()
        .find(|curve| curve.target() == "Parameter")
        .map(|curve| curve.id().to_owned())
        .expect("idle motion has a parameter curve");
    let index = model
        .runtime()
        .parameter_index(&target)
        .expect("motion parameter exists on model");

    let mut player = MotionPlayer::new(motion);
    player.tick(0.5);
    player.apply(model.runtime_mut());
    let first = model.runtime().parameter_value_by_index(index).unwrap();

    player.tick(0.5);
    player.apply(model.runtime_mut());
    let second = model.runtime().parameter_value_by_index(index).unwrap();

    assert!(first.is_finite() && second.is_finite());
}

#[test]
fn non_looping_motion_finishes_after_duration() {
    let motion = Motion3::from_json_str(
        r#"{
            "Version": 3,
            "Meta": { "Duration": 1.0, "Fps": 30.0, "Loop": false },
            "Curves": [
                { "Target": "Parameter", "Id": "ParamAngleX", "Segments": [0.0, 0.0, 0, 1.0, 10.0] }
            ]
        }"#,
    )
    .unwrap();

    let mut player = MotionPlayer::new(motion);
    assert!(!player.is_finished());
    player.tick(0.5);
    assert!(!player.is_finished());
    player.tick(0.6);
    assert!(player.is_finished());
    assert_eq!(player.time(), 1.0);
}

#[test]
fn looping_motion_wraps_time() {
    let motion = Motion3::from_json_str(
        r#"{
            "Version": 3,
            "Meta": { "Duration": 1.0, "Fps": 30.0, "Loop": true },
            "Curves": [
                { "Target": "Parameter", "Id": "ParamAngleX", "Segments": [0.0, 0.0, 0, 1.0, 10.0] }
            ]
        }"#,
    )
    .unwrap();

    let mut player = MotionPlayer::new(motion);
    player.tick(1.5);
    assert!(!player.is_finished());
    assert!((player.time() - 0.5).abs() < 0.0001);
}

fn hiyori_mesh_snapshot(model: &mocari::assets::RuntimeModel) -> Vec<Vec<[f32; 2]>> {
    model
        .runtime()
        .meshes()
        .iter()
        .map(|mesh| mesh.vertices().iter().map(|v| v.position()).collect())
        .collect()
}

#[test]
fn hiyori_distinct_bindings_drive_distinct_parameters() {
    let mut model = load_model_runtime("assets/models/Hiyori/Hiyori.model3.json").unwrap();
    let baseline = hiyori_mesh_snapshot(&model);

    model.runtime_mut().set_parameter("ParamRibbon", 1.0);
    model.runtime_mut().update_meshes().unwrap();
    let ribbon = hiyori_mesh_snapshot(&model);
    assert_ne!(
        baseline, ribbon,
        "ParamRibbon should deform the Hiyori mesh"
    );

    model.runtime_mut().reset_parameters();
    model.runtime_mut().set_parameter("ParamSkirt2", 1.0);
    model.runtime_mut().update_meshes().unwrap();
    let skirt = hiyori_mesh_snapshot(&model);
    assert_ne!(baseline, skirt, "ParamSkirt2 should deform the Hiyori mesh");

    assert_ne!(
        ribbon, skirt,
        "distinct parameters must drive distinct deformations"
    );
}

#[test]
fn zeroing_a_part_hides_its_drawables() {
    let mut model = load_model_runtime("assets/models/Hiyori/Hiyori.model3.json").unwrap();
    let baseline_visible = model
        .runtime()
        .meshes()
        .iter()
        .filter(|mesh| mesh.opacity() > 0.0)
        .count();
    assert!(baseline_visible > 0);

    let part_ids: Vec<String> = model.runtime().part_ids().to_vec();
    let mut hid_some = false;
    for part_id in part_ids {
        model.runtime_mut().reset_part_opacities();
        model.runtime_mut().set_part_opacity(&part_id, 0.0);
        model.runtime_mut().update_meshes().unwrap();
        let visible = model
            .runtime()
            .meshes()
            .iter()
            .filter(|mesh| mesh.opacity() > 0.0)
            .count();
        if visible < baseline_visible {
            hid_some = true;
            assert!(visible > 0, "a single part should not hide the whole model");
            break;
        }
    }
    assert!(hid_some, "no part hid any drawable");
}

#[test]
fn mao_drawables_carry_non_identity_multiply_and_screen_colors() {
    let model = load_model_runtime("assets/models/Mao/Mao.model3.json").unwrap();
    let non_identity = model
        .runtime()
        .meshes()
        .iter()
        .filter(|mesh| {
            mesh.multiply_color() != [1.0, 1.0, 1.0] || mesh.screen_color() != [0.0, 0.0, 0.0]
        })
        .count();
    assert!(
        non_identity > 0,
        "Mao should expose per-drawable color keyforms"
    );
}

#[test]
fn expression_player_applies_faded_expression_parameters() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let expression = Expression3::from_json_str(
        r#"{
            "Type": "Live2D Expression",
            "Parameters": [
                { "Id": "ParamAngleX", "Value": 10.0, "Blend": "Add" }
            ]
        }"#,
    )
    .unwrap();
    let index = model.runtime().parameter_index("ParamAngleX").unwrap();
    let default = model.runtime().parameter_value_by_index(index).unwrap();

    let mut player = ExpressionPlayer::new(expression);
    player.tick(0.5);
    player.apply(model.runtime_mut());

    let value = model.runtime().parameter_value_by_index(index).unwrap();
    assert_close(value, default + 5.0);
}

#[test]
fn expression_manager_fades_out_previous_expression_when_playing_next() {
    let mut model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    let first = Expression3::from_json_str(
        r#"{
            "Type": "Live2D Expression",
            "Parameters": [
                { "Id": "ParamAngleX", "Value": 10.0, "Blend": "Add" }
            ]
        }"#,
    )
    .unwrap();
    let second = Expression3::from_json_str(
        r#"{
            "Type": "Live2D Expression",
            "Parameters": [
                { "Id": "ParamAngleX", "Value": -4.0, "Blend": "Add" }
            ]
        }"#,
    )
    .unwrap();
    let index = model.runtime().parameter_index("ParamAngleX").unwrap();
    let default = model.runtime().parameter_value_by_index(index).unwrap();

    let mut manager = ExpressionManager::new();
    manager.play(first);
    manager.tick(1.0);
    model.runtime_mut().reset_parameters();
    manager.apply(model.runtime_mut());
    assert_close(
        model.runtime().parameter_value_by_index(index).unwrap(),
        default + 10.0,
    );

    manager.play(second);
    manager.tick(0.5);
    model.runtime_mut().reset_parameters();
    manager.apply(model.runtime_mut());

    let value = model.runtime().parameter_value_by_index(index).unwrap();
    assert_close(value, default + (10.0 * 0.5) + (-4.0 * 0.5));
    assert_eq!(manager.active_expression_count(), 2);

    manager.tick(0.5);
    assert_eq!(manager.active_expression_count(), 1);
}

#[test]
fn load_expression_reads_exp3_asset() {
    let expression = load_expression("assets/models/Haru/expressions/F01.exp3.json").unwrap();

    assert_eq!(expression.kind(), "Live2D Expression");
    assert_eq!(expression.parameters()[0].id(), "ParamMouthForm");
}

#[test]
fn mao_drawable_colors_match_core_default_pose() {
    let model = load_model_runtime("assets/models/Mao/Mao.model3.json").unwrap();
    let meshes = model.runtime().meshes();

    assert_color_close(meshes[45].multiply_color(), [1.0, 1.0, 1.0]);
    assert_color_close(meshes[45].screen_color(), [0.0, 0.0, 0.0]);
    assert_color_close(meshes[138].multiply_color(), [1.0, 1.0, 1.0]);
    assert_color_close(meshes[138].screen_color(), [1.0, 0.454_901_96, 0.513_725_5]);
}

#[test]
fn legacy_model_without_color_keyforms_defaults_to_identity() {
    let model = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
    for mesh in model.runtime().meshes() {
        assert_eq!(mesh.multiply_color(), [1.0, 1.0, 1.0]);
        assert_eq!(mesh.screen_color(), [0.0, 0.0, 0.0]);
    }
}

#[test]
fn deformer_opacity_hides_rest_state_effects() {
    let model = load_model_runtime("assets/models/Rice/Rice.model3.json").unwrap();
    let meshes = model.runtime().meshes();
    for &index in &[151usize, 152, 153] {
        assert_eq!(
            meshes[index].opacity(),
            0.0,
            "magic-circle drawable {index} must be hidden by deformer opacity at rest"
        );
    }
}

#[test]
fn default_pose_hides_redundant_arm_via_pose_groups() {
    let model = load_model_runtime("assets/models/Hiyori/Hiyori.model3.json").unwrap();
    let hidden = model
        .runtime()
        .meshes()
        .iter()
        .filter(|mesh| mesh.opacity() == 0.0)
        .count();
    let visible = model
        .runtime()
        .meshes()
        .iter()
        .filter(|mesh| mesh.opacity() > 0.0)
        .count();

    assert!(
        hidden > 0,
        "pose group should hide the redundant arm at rest"
    );
    assert!(visible > 0, "the selected arm and body must stay visible");
}

fn assert_color_close(actual: [f32; 3], expected: [f32; 3]) {
    for (actual, expected) in actual.into_iter().zip(expected) {
        assert!((actual - expected).abs() < 0.0001);
    }
}

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