#![allow(deprecated)]

use approx::assert_relative_eq;
use multibody_dynamics::math_functions::skew;
use multibody_dynamics::multibody::*;
use nalgebra as na;

fn scalar_link(mass: f64) -> LinkProperties {
    LinkProperties {
        mass: Some(mass),
        r_com: Some(na::Vector3::zeros()),
        inertia3: Some(na::Matrix3::identity()),
        ..LinkProperties::default()
    }
}

fn simple_config<const NUM_BODIES: usize, const NUM_DOFS: usize>(
    joint_types: Vec<JointType>,
    parent: Vec<u16>,
) -> MultiBodyConfig<NUM_BODIES, NUM_DOFS> {
    MultiBodyConfig {
        topology: Topology {
            offset_matrices: vec![na::Isometry3::identity(); NUM_BODIES],
            joint_types,
            parent,
        },
        link_props: Some(vec![scalar_link(1.0); NUM_BODIES]),
        env: Environment::default(),
    }
}

#[test]
fn rejects_wrong_dof_count_before_matrix_indexing() {
    let cfg = simple_config::<1, 1>(vec![JointType::SixDOF], vec![0]);

    let err = MultiBody::<1, 1>::from_config(cfg).err().unwrap();
    assert_eq!(err, "NUM_DOFS must equal sum of joint dimensions");
}

#[test]
fn rejects_invalid_parent_topology() {
    let later_parent = simple_config::<2, 2>(vec![JointType::Revolute(Axis::Z); 2], vec![2, 0]);
    assert_eq!(
        MultiBody::<2, 2>::from_config(later_parent).err().unwrap(),
        "parent must reference an earlier body"
    );

    let out_of_range = simple_config::<2, 2>(vec![JointType::Revolute(Axis::Z); 2], vec![0, 3]);
    assert_eq!(
        MultiBody::<2, 2>::from_config(out_of_range).err().unwrap(),
        "parent index out of range"
    );
}

#[test]
fn legacy_new_returns_err_for_short_optional_vectors() {
    let result = MultiBody::<2, 2>::new(
        vec![na::Isometry3::identity(); 2],
        Some(vec![na::SMatrix::<f64, 6, 6>::identity()]),
        None,
        None,
        vec![JointType::Revolute(Axis::Z); 2],
        vec![0, 1],
        na::Vector3::zeros(),
        None,
        None,
        None,
        None,
        None,
    );

    assert_eq!(result.err().unwrap(), "mass_matrices length mismatch");
}

#[test]
fn rejects_partial_scalar_mass_properties() {
    let mut cfg = simple_config::<1, 1>(vec![JointType::Revolute(Axis::Z)], vec![0]);
    cfg.link_props = Some(vec![LinkProperties {
        mass: Some(1.0),
        r_com: Some(na::Vector3::zeros()),
        inertia3: None,
        ..LinkProperties::default()
    }]);

    assert_eq!(
        MultiBody::<1, 1>::from_config(cfg).err().unwrap(),
        "mass, r_com, and inertia3 must all be provided together"
    );
}

#[test]
fn legacy_new_preserves_missing_rho_as_zero_density() {
    let mass = 2.0;
    let gravity = na::Vector3::new(0.0, 0.0, -9.81);
    let mb = MultiBody::<1, 1>::new(
        vec![na::Isometry3::identity()],
        None,
        None,
        Some(vec![na::Matrix3::identity()]),
        vec![JointType::Revolute(Axis::Z)],
        vec![0],
        gravity,
        Some(vec![na::Vector3::zeros()]),
        Some(vec![na::Vector3::zeros()]),
        Some(vec![mass]),
        Some(vec![1.0]),
        None,
    )
    .unwrap();

    let force =
        mb.compute_hydrostatic_force(&na::UnitQuaternion::identity(), &na::Vector3::zeros(), 0);

    assert_relative_eq!(force.fixed_rows::<3>(0).into_owned(), mass * gravity);
}

#[test]
fn try_minimal_configuration_validates_input_lengths() {
    let mb = MultiBody::<2, 7>::from_config(simple_config::<2, 7>(
        vec![JointType::SixDOF, JointType::Revolute(Axis::Z)],
        vec![0, 1],
    ))
    .unwrap();

    let no_six_dof_vars: Vec<na::Isometry3<f64>> = Vec::new();
    let scalar = na::SVector::<f64, 1>::zeros();
    assert_eq!(
        mb.try_minimal_to_homogeneous_configuration(&no_six_dof_vars, &scalar)
            .unwrap_err(),
        "six_dof_vars length mismatch"
    );

    let base = na::Isometry3::identity();
    let too_many_scalars = na::SVector::<f64, 2>::zeros();
    assert_eq!(
        mb.try_minimal_to_homogeneous_configuration(&base, &too_many_scalars)
            .unwrap_err(),
        "scalar_joint_vars length mismatch"
    );
}

#[test]
fn mass6_supplies_gravity_mass_properties() {
    let mass = 2.0;
    let r_com = na::Vector3::new(0.1, -0.2, 0.3);
    let mut mass6 = na::SMatrix::<f64, 6, 6>::zeros();
    mass6
        .fixed_view_mut::<3, 3>(0, 0)
        .copy_from(&(mass * na::Matrix3::identity()));
    mass6
        .fixed_view_mut::<3, 3>(0, 3)
        .copy_from(&(-mass * skew(&r_com)));
    mass6
        .fixed_view_mut::<3, 3>(3, 0)
        .copy_from(&(mass * skew(&r_com)));
    mass6
        .fixed_view_mut::<3, 3>(3, 3)
        .copy_from(&na::Matrix3::identity());

    let mb = MultiBody::<1, 6>::from_config(MultiBodyConfig {
        topology: Topology {
            offset_matrices: vec![na::Isometry3::identity()],
            joint_types: vec![JointType::SixDOF],
            parent: vec![0],
        },
        link_props: Some(vec![LinkProperties {
            mass6: Some(mass6),
            ..LinkProperties::default()
        }]),
        env: Environment {
            gravity: na::Vector3::new(0.0, 0.0, -9.81),
            rho: 1025.0,
        },
    })
    .unwrap();

    let force =
        mb.compute_hydrostatic_force(&na::UnitQuaternion::identity(), &na::Vector3::zeros(), 0);
    let expected_linear = mass * na::Vector3::new(0.0, 0.0, -9.81);
    let expected_rotational = mass * skew(&r_com) * na::Vector3::new(0.0, 0.0, -9.81);

    assert_relative_eq!(force.fixed_rows::<3>(0).into_owned(), expected_linear);
    assert_relative_eq!(force.fixed_rows::<3>(3).into_owned(), expected_rotational);
}

#[test]
fn try_regressor_rejects_wrong_joint_shape() {
    let mb = MultiBody::<1, 1>::from_config(simple_config::<1, 1>(
        vec![JointType::Revolute(Axis::Z)],
        vec![0],
    ))
    .unwrap();
    let body = |_: &na::Isometry3<f64>,
                _: &na::SVector<f64, 6>,
                _: &na::SVector<f64, 6>,
                _: &na::SVector<f64, 6>|
     -> na::SMatrix<f64, 6, 2> { na::SMatrix::<f64, 6, 2>::zeros() };
    let joint = |_: &na::Isometry3<f64>,
                 _: JointKinArg,
                 _: JointKinArg,
                 _: JointKinArg|
     -> JointRegressorOut<2> {
        JointRegressorOut::Matrix(na::SMatrix::<f64, 6, 2>::zeros())
    };
    let body_ref: &BodyRegressorFn<2> = &body;
    let joint_ref: &JointRegressorFn<2> = &joint;
    let conf = vec![na::Isometry3::identity()];
    let mu = na::SVector::<f64, 1>::zeros();

    assert_eq!(
        mb.try_compute_regressor_matrix([body_ref], [joint_ref], &conf, &mu, &mu, &mu)
            .unwrap_err(),
        "scalar joint regressor returned Matrix"
    );
}

#[test]
fn try_regressor_validates_configuration_length() {
    let mb = MultiBody::<1, 1>::from_config(simple_config::<1, 1>(
        vec![JointType::Revolute(Axis::Z)],
        vec![0],
    ))
    .unwrap();
    let body = |_: &na::Isometry3<f64>,
                _: &na::SVector<f64, 6>,
                _: &na::SVector<f64, 6>,
                _: &na::SVector<f64, 6>|
     -> na::SMatrix<f64, 6, 2> { na::SMatrix::<f64, 6, 2>::zeros() };
    let joint = |_: &na::Isometry3<f64>,
                 _: JointKinArg,
                 _: JointKinArg,
                 _: JointKinArg|
     -> JointRegressorOut<2> {
        JointRegressorOut::Row(na::SMatrix::<f64, 1, 2>::zeros())
    };
    let body_ref: &BodyRegressorFn<2> = &body;
    let joint_ref: &JointRegressorFn<2> = &joint;
    let conf: Vec<na::Isometry3<f64>> = Vec::new();
    let mu = na::SVector::<f64, 1>::zeros();

    assert_eq!(
        mb.try_compute_regressor_matrix([body_ref], [joint_ref], &conf, &mu, &mu, &mu)
            .unwrap_err(),
        "conf length mismatch"
    );
}
