#![allow(clippy::float_cmp, clippy::too_many_lines)]

// Exact BSD-3-Clause fixture provenance at LibreMetaverse
// 2aa70bb68513b39795da5d13c88f31b86e85a3ba (see LICENSE.md):
// AnimationTrackTests.cs 9f83830d4278906e4edf8d5ad71d59f96e63ae5860e3b3cc9e191de05741f3d0
// AnimeshPlayerTests.cs b79a4f667c73c30376ee6f550436b26e76efd0fa0383d67bc388a74f07f679ce
// AnimeshSkinningTests.cs 72f51e5dc4fa59e87c150aacb93aae26aabad71fb45e1830cd46e7c5216e1266
// AvatarManagerTests.cs f7547d5caed1d4669eccfad57836f4d2243c8275b1d182e7cb363660b3eabb68
// LindenSkeletonTests.cs 23f7e3ffaf8c4422e8dc2e42ed4223b16175228153aba8eb69cd7ef7e2037b76

use crate::animesh::{AnimationTrack, AnimeshPlayer, JointPose};
use crate::rendering::{
    Face, FaceMask, Joint, JointSupportCategory, LindenSkeleton, MeshSkinData, Vertex, VertexWeight,
};
use crate::{AnimeshSkinning, BinBVHAnimationReader, GridClient, PrimitiveTextureEntryFace};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{Error, Matrix4, UUID, Vector2, Vector3};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

fn random_uuid() -> UUID {
    UUID::random().expect("UUID.Random")
}

fn vector3(x: f32, y: f32, z: f32) -> Vector3 {
    Vector3::new_with_single_single_single(x, y, z).expect("Vector3 constructor")
}

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

fn make_animation(
    in_point: f32,
    out_point: f32,
    loop_: bool,
    ease_in: f32,
    ease_out: f32,
) -> BinBVHAnimationReader {
    let mut data = Vec::new();
    data.extend_from_slice(&1_u16.to_le_bytes());
    data.extend_from_slice(&0_u16.to_le_bytes());
    data.extend_from_slice(&2_i32.to_le_bytes());
    data.extend_from_slice(&(out_point - in_point).to_le_bytes());
    data.push(0);
    data.extend_from_slice(&in_point.to_le_bytes());
    data.extend_from_slice(&out_point.to_le_bytes());
    data.extend_from_slice(&i32::from(loop_).to_le_bytes());
    data.extend_from_slice(&ease_in.to_le_bytes());
    data.extend_from_slice(&ease_out.to_le_bytes());
    data.extend_from_slice(&0_u32.to_le_bytes());
    data.extend_from_slice(&0_u32.to_le_bytes());
    BinBVHAnimationReader::new(data).expect("BinBVHAnimationReader constructor")
}

fn make_track(
    in_point: f32,
    out_point: f32,
    loop_: bool,
    ease_in: f32,
    ease_out: f32,
) -> AnimationTrack {
    let mut track = AnimationTrack::new(random_uuid()).expect("AnimationTrack constructor");
    track.set_data(Some(make_animation(
        in_point, out_point, loop_, ease_in, ease_out,
    )));
    track
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.DecodeRotation_ZeroVector_ReturnsIdentity::test fd67db1a9689e3e624fbe8bea98d6278774e4d67f85e611aac6f76105240a6b1 translated
#[test]
fn animation_track_decode_rotation_zero_vector_returns_identity() {
    let quaternion = AnimationTrack::decode_rotation(Vector3::zero()).expect("DecodeRotation");
    assert_close(quaternion.w, 1.0, 1.0e-5);
    assert_close(quaternion.x, 0.0, 1.0e-5);
    assert_close(quaternion.y, 0.0, 1.0e-5);
    assert_close(quaternion.z, 0.0, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.DecodeRotation_Result_IsNormalized::test 880fdfb75fd9318afa21d3b5296790b3ebad944b43a998a924691fa74efbb2fe translated
#[test]
fn animation_track_decode_rotation_result_is_normalized() {
    let vectors = [
        vector3(0.1, 0.2, 0.3),
        vector3(0.5, 0.0, 0.0),
        vector3(0.0, 0.0, 0.99),
        Vector3::zero(),
    ];
    for value in vectors {
        let quaternion = AnimationTrack::decode_rotation(value).expect("DecodeRotation");
        let magnitude = (quaternion.x * quaternion.x
            + quaternion.y * quaternion.y
            + quaternion.z * quaternion.z
            + quaternion.w * quaternion.w)
            .sqrt();
        assert_close(magnitude, 1.0, 1.0e-5);
    }
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.DecodeRotation_WIsRecoveredFromXyz::test 738fd51ea71da85b3b2d43ec30b8e9883b5364c9cbd7c3cbe87de3ea6778daa8 translated
#[test]
fn animation_track_decode_rotation_w_is_recovered_from_xyz() {
    let quaternion =
        AnimationTrack::decode_rotation(vector3(0.6, 0.0, 0.0)).expect("DecodeRotation");
    assert_close(quaternion.x, 0.6, 1.0e-5);
    assert_close(quaternion.w, 0.8, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.DecodeRotation_WAlwaysNonNegative::test 679c12e5cdaac7a970f15606229b1843d82e68b28883f87cc5d73567b04c9b57 translated
#[test]
fn animation_track_decode_rotation_w_always_non_negative() {
    for value in [
        vector3(0.5, 0.5, 0.5),
        vector3(-0.5, -0.5, -0.5),
        vector3(0.0, 0.0, 0.9),
    ] {
        let quaternion = AnimationTrack::decode_rotation(value).expect("DecodeRotation");
        assert!(quaternion.w >= 0.0);
    }
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.Advance_WithNullData_DoesNotChangeCurrentTime::test cfd6f6ed1f568fe0dda207cc3222c487a3f8b509a5aa0743109c04032a29b808 translated
#[test]
fn animation_track_advance_with_null_data_does_not_change_current_time() {
    let mut track = AnimationTrack::new(random_uuid()).expect("AnimationTrack constructor");
    track.advance(1.0).expect("Advance");
    assert_eq!(track.current_time(), 0.0);
    assert!(!track.is_finished());
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EaseWeight_WithNullData_ReturnsZero::test 7e74e5323ca9870782821a6985f446030d1a8858182bdf82d7718617eacd8ef5 translated
#[test]
fn animation_track_ease_weight_with_null_data_returns_zero() {
    let track = AnimationTrack::new(random_uuid()).expect("AnimationTrack constructor");
    assert_eq!(track.ease_weight(), 0.0);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EvaluatePose_WithNullData_LeavesEmptyDictionaryUnchanged::test 287210ac2f57c80bd77688a97720b0a7ca63118ce3c8da591274b53aeef88769 translated
#[test]
fn animation_track_evaluate_pose_with_null_data_leaves_empty_dictionary_unchanged() {
    let track = AnimationTrack::new(random_uuid()).expect("AnimationTrack constructor");
    let mut pose = HashMap::<String, JointPose>::new();
    track.evaluate_pose(&mut pose).expect("EvaluatePose");
    assert!(pose.is_empty());
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.Advance_NonLooping_BeforeOutPoint_AdvancesTime::test da6ab7efced5ff86e83c320685d6fef33df2a2eb62795206b9f78e87a51ca93d translated
#[test]
fn animation_track_advance_non_looping_before_out_point_advances_time() {
    let mut track = make_track(0.0, 10.0, false, 0.0, 0.0);
    track.advance(3.0).expect("Advance");
    assert_close(track.current_time(), 3.0, 1.0e-5);
    assert!(!track.is_finished());
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.Advance_NonLooping_PastOutPoint_ClampsAndSetsFinished::test 02acfe2c10a27a6222e8d222de47255c68afc46ba97cc9319f8239af021fb365 translated
#[test]
fn animation_track_advance_non_looping_past_out_point_clamps_and_sets_finished() {
    let mut track = make_track(0.0, 5.0, false, 0.0, 0.0);
    track.advance(10.0).expect("Advance");
    assert_close(track.current_time(), 5.0, 1.0e-5);
    assert!(track.is_finished());
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.Advance_Looping_WrapsAroundToInPoint::test e5e29dec8ece4c3ed19691aa0da0b0199de14902184a91548451c40390af4f8e translated
#[test]
fn animation_track_advance_looping_wraps_around_to_in_point() {
    let mut track = make_track(1.0, 4.0, true, 0.0, 0.0);
    track.advance(6.0).expect("Advance");
    assert_close(track.current_time(), 3.0, 1.0e-4);
    assert!(!track.is_finished());
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.Advance_Finished_DoesNotAdvanceFurther::test 17680a41433f4f274645b15f49c4b4f7efea42af4542ee3f6c2e16aa8614f994 translated
#[test]
fn animation_track_advance_finished_does_not_advance_further() {
    let mut track = make_track(0.0, 5.0, false, 0.0, 0.0);
    track.advance(10.0).expect("Advance");
    let stopped_at = track.current_time();
    track.advance(5.0).expect("Advance");
    assert_eq!(track.current_time(), stopped_at);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EaseWeight_InMiddle_IsOne::test 49a02b2910e60254f80760d40b62b41fc02a4747d85a7b41ee8d082d0352d3cf translated
#[test]
fn animation_track_ease_weight_in_middle_is_one() {
    let mut track = make_track(0.0, 10.0, false, 1.0, 1.0);
    track.advance(5.0).expect("Advance");
    assert_close(track.ease_weight(), 1.0, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EaseWeight_DuringEaseIn_IsFractional::test 419c26af8c1a989a7f99d1b32ca28a41768d7171a4452466f909e1fe61cea186 translated
#[test]
fn animation_track_ease_weight_during_ease_in_is_fractional() {
    let mut track = make_track(0.0, 10.0, false, 2.0, 0.0);
    track.advance(1.0).expect("Advance");
    assert_close(track.ease_weight(), 0.5, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EaseWeight_DuringEaseOut_DecreasesTowardZero::test 18fd883c525f528f0ead69d3df187b106980ff939091c3ad56b156c2d05c91b0 translated
#[test]
fn animation_track_ease_weight_during_ease_out_decreases_toward_zero() {
    let mut track = make_track(0.0, 10.0, false, 0.0, 4.0);
    track.advance(8.0).expect("Advance");
    assert_close(track.ease_weight(), 0.5, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimationTrackTests.cs::AnimationTrackTests.EaseWeight_NoEaseCurves_IsAlwaysOne::test 49421472fcd3dcdb56cb9bb2448b2808bf76757454a67f2ced7524d9a6f89d6e translated
#[test]
fn animation_track_ease_weight_no_ease_curves_is_always_one() {
    let mut track = make_track(0.0, 10.0, false, 0.0, 0.0);
    track.advance(5.0).expect("Advance");
    assert_eq!(track.ease_weight(), 1.0);
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.GetOrAddTrack_CreatesNewTrack::test b7fcdc9f7c02167a6307facaf720390298d68e9c909792d4feab3b78d9b87d7f translated
#[test]
fn animesh_player_get_or_add_track_creates_new_track() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let id = random_uuid();
    let track = player.get_or_add_track(id).expect("GetOrAddTrack");
    assert!(track.animation_id().equals_with_uuid(id));
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.GetOrAddTrack_SameId_ReturnsSameTrack::test aaec517ee073f26f6ed324cb4391a945221ff0349348bba3757b2378b38df610 translated
#[test]
fn animesh_player_get_or_add_track_same_id_returns_same_track() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let id = random_uuid();
    let first = player.get_or_add_track(id).expect("GetOrAddTrack");
    let second = player.get_or_add_track(id).expect("GetOrAddTrack");
    assert!(Arc::ptr_eq(&first, &second));
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.GetOrAddTrack_DifferentIds_ReturnDifferentTracks::test fe47b6dabc58df9078751501e3bb7e65b0aeb4c0929f4523aded0cb624d64bb1 translated
#[test]
fn animesh_player_get_or_add_track_different_ids_return_different_tracks() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let first = player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    let second = player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    assert!(!Arc::ptr_eq(&first, &second));
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.TrackCount_IncreasesWithNewTracks::test 241b4b3606c0d5c8625701bd21ec41438ac9a410eb11620d1609ddd4d2d58dda translated
#[test]
fn animesh_player_track_count_increases_with_new_tracks() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    assert_eq!(player.track_count(), 2);
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.TrackCount_StableForDuplicateId::test b36dfe47c90dd24fe489ae408cfb299f7214eddb3dc9d5ba42830ab9950d9268 translated
#[test]
fn animesh_player_track_count_stable_for_duplicate_id() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let id = random_uuid();
    player.get_or_add_track(id).expect("GetOrAddTrack");
    player.get_or_add_track(id).expect("GetOrAddTrack");
    assert_eq!(player.track_count(), 1);
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.RetainOnly_RemovesAbsentIds::test d4f15706802d59901e8f07fc17f1d11063b6c182a7a40833d988191edd556f46 translated
#[test]
fn animesh_player_retain_only_removes_absent_ids() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let keep = random_uuid();
    let drop = random_uuid();
    player.get_or_add_track(keep).expect("GetOrAddTrack");
    player.get_or_add_track(drop).expect("GetOrAddTrack");
    player
        .retain_only(HashSet::from([keep]))
        .expect("RetainOnly");
    assert_eq!(player.track_count(), 1);
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.RetainOnly_KeepsPresentIds::test eb568b72c8930dea6678a4b54ff2dd47101cef0ed8f6380875cb3fdd27ec6ad2 translated
#[test]
fn animesh_player_retain_only_keeps_present_ids() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    let id = random_uuid();
    let original = player.get_or_add_track(id).expect("GetOrAddTrack");
    player.retain_only(HashSet::from([id])).expect("RetainOnly");
    let retained = player.get_or_add_track(id).expect("GetOrAddTrack");
    assert!(Arc::ptr_eq(&original, &retained));
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.RetainOnly_EmptySet_RemovesAllTracks::test 8a8ee666933d4bd920c1d239f5180b9b4ee0d6eb7be67a6965fe29ca6f22edff translated
#[test]
fn animesh_player_retain_only_empty_set_removes_all_tracks() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    player.retain_only(HashSet::new()).expect("RetainOnly");
    assert_eq!(player.track_count(), 0);
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.Update_WithNoTracks_DoesNotThrow::test 866542d8e7e14a1f52a4f6b79aa8284cc5347d1a78a44ed9b4950b67078ff592 translated
#[test]
fn animesh_player_update_with_no_tracks_does_not_throw() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    player.update(0.016).expect("Update");
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.Update_WithTracksButNoData_DoesNotThrow::test b29bcf3676a3fca8d1e352ff672350a7bcb92a101dd6f622c53ee2b9a64a3536 translated
#[test]
fn animesh_player_update_with_tracks_but_no_data_does_not_throw() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    player.update(0.016).expect("Update");
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.EvaluatePose_NoLoadedData_ReturnsEmptyDictionary::test 3427b5c94f64a7465a8f3d4d13a1b67e2d002df81a4e736398e1934c8b08cb4b translated
#[test]
fn animesh_player_evaluate_pose_no_loaded_data_returns_empty_dictionary() {
    let mut player = AnimeshPlayer::new(random_uuid()).expect("AnimeshPlayer constructor");
    player
        .get_or_add_track(random_uuid())
        .expect("GetOrAddTrack");
    let pose = player.evaluate_pose().expect("EvaluatePose");
    assert!(pose.is_empty());
}

// parity-case: LibreMetaverse.Tests/AnimeshPlayerTests.cs::AnimeshPlayerTests.ObjectID_StoresConstructorValue::test f2a2efabccbf63c110336cb5785487eb65ee7347e1ff0bb499295e4d5e255654 translated
#[test]
fn animesh_player_object_id_stores_constructor_value() {
    let id = random_uuid();
    let player = AnimeshPlayer::new(id).expect("AnimeshPlayer constructor");
    assert!(player.object_id().equals_with_uuid(id));
}

fn make_joint(name: &str, children: Vec<Joint>) -> Joint {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name(name.to_owned());
    joint.set_bone(Some(children));
    joint
}

fn make_skeleton(root_name: &str, root_children: Vec<Joint>) -> LindenSkeleton {
    let mut skeleton = LindenSkeleton::new().expect("LindenSkeleton constructor");
    skeleton.set_bone(make_joint(root_name, root_children));
    skeleton
}

fn make_single_joint_skeleton(joint_name: &str) -> LindenSkeleton {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name(joint_name.to_owned());
    joint.base.set_pos(vec![0.0, 0.0, 0.0]);
    joint.base.set_rot(vec![0.0, 0.0, 0.0]);
    let mut skeleton = LindenSkeleton::new().expect("LindenSkeleton constructor");
    skeleton.set_bone(joint);
    skeleton
}

fn identity_floats() -> Vec<f32> {
    vec![
        1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
    ]
}

fn make_skin_data(joint_name: &str) -> MeshSkinData {
    let mut skin_data = MeshSkinData::new().expect("MeshSkinData constructor");
    skin_data.joint_names = vec![joint_name.to_owned()];
    skin_data.inverse_bind_matrices = identity_floats();
    skin_data.bind_shape_matrix = identity_floats();
    skin_data
}

fn assert_matrix_identity(matrix: Matrix4, tolerance: f32) {
    let identity = Matrix4::identity();
    assert_close(matrix.m11, identity.m11, tolerance);
    assert_close(matrix.m12, identity.m12, tolerance);
    assert_close(matrix.m13, identity.m13, tolerance);
    assert_close(matrix.m14, identity.m14, tolerance);
    assert_close(matrix.m21, identity.m21, tolerance);
    assert_close(matrix.m22, identity.m22, tolerance);
    assert_close(matrix.m23, identity.m23, tolerance);
    assert_close(matrix.m24, identity.m24, tolerance);
    assert_close(matrix.m31, identity.m31, tolerance);
    assert_close(matrix.m32, identity.m32, tolerance);
    assert_close(matrix.m33, identity.m33, tolerance);
    assert_close(matrix.m34, identity.m34, tolerance);
    assert_close(matrix.m41, identity.m41, tolerance);
    assert_close(matrix.m42, identity.m42, tolerance);
    assert_close(matrix.m43, identity.m43, tolerance);
    assert_close(matrix.m44, identity.m44, tolerance);
}

fn vertex(position: Vector3) -> Vertex {
    Vertex {
        position,
        normal: Vector3::zero(),
        tex_coord: Vector2::zero(),
    }
}

fn face(vertices: Vec<Vertex>, weights: Option<Vec<VertexWeight>>) -> Face {
    Face {
        begin_s: 0,
        begin_t: 0,
        center: Vector3::zero(),
        edge: Vec::new(),
        id: 0,
        indices: Vec::new(),
        mask: FaceMask(0),
        max_extent: Vector3::zero(),
        min_extent: Vector3::zero(),
        normalized_scale: Vector3::zero(),
        num_s: 0,
        num_t: 0,
        tex_coords1: None,
        texture_face: PrimitiveTextureEntryFace,
        user_data: Object::from(""),
        vertices,
        weights,
    }
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_NullPose_ThrowsArgumentNullException::test 877768309d784145e02873759fa3726bc2ca6eb4b9d35b0974b98d3c0a06b97a translated
#[test]
fn animesh_skinning_compute_matrices_null_pose_throws_argument_null() {
    let skeleton = make_single_joint_skeleton("mRoot");
    let skin_data = make_skin_data("mRoot");
    let Err(error) =
        AnimeshSkinning::compute_skinning_matrices(None, Some(skeleton), Some(skin_data))
    else {
        panic!("null pose must fail");
    };
    assert_eq!(error, Error::ArgumentNull);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_NullSkeleton_ThrowsArgumentNullException::test 97d7eef733424588868943400ef2a9664243d5b0c1df884e3cf3e2bc3fa299d6 translated
#[test]
fn animesh_skinning_compute_matrices_null_skeleton_throws_argument_null() {
    let pose = HashMap::<String, JointPose>::new();
    let skin_data = make_skin_data("mRoot");
    let Err(error) = AnimeshSkinning::compute_skinning_matrices(Some(pose), None, Some(skin_data))
    else {
        panic!("null skeleton must fail");
    };
    assert_eq!(error, Error::ArgumentNull);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_NullSkinData_ThrowsArgumentNullException::test a0e41b5457d413f4fb8da3d159c74318bf9b74333ce8962612610c862fca8b06 translated
#[test]
fn animesh_skinning_compute_matrices_null_skin_data_throws_argument_null() {
    let pose = HashMap::<String, JointPose>::new();
    let skeleton = make_single_joint_skeleton("mRoot");
    let Err(error) = AnimeshSkinning::compute_skinning_matrices(Some(pose), Some(skeleton), None)
    else {
        panic!("null skin data must fail");
    };
    assert_eq!(error, Error::ArgumentNull);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_EmptySkinData_ReturnsEmptyArray::test 412f936f350e1aba601fb736a91087c89976c751a38e1653eec24a958f7c71ba translated
#[test]
fn animesh_skinning_compute_matrices_empty_skin_data_returns_empty_array() {
    let pose = HashMap::<String, JointPose>::new();
    let skeleton = make_single_joint_skeleton("mRoot");
    let mut skin_data = MeshSkinData::new().expect("MeshSkinData constructor");
    skin_data.joint_names = Vec::new();
    skin_data.inverse_bind_matrices = Vec::new();
    let matrices =
        AnimeshSkinning::compute_skinning_matrices(Some(pose), Some(skeleton), Some(skin_data))
            .expect("ComputeSkinningMatrices");
    assert!(matrices.is_empty());
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_JointNotInSkeleton_ProducesIdentity::test c0807fdb1975324c81bda29c46ab84faaad2d19b61b257180f47b6eaa2800146 translated
#[test]
fn animesh_skinning_compute_matrices_joint_not_in_skeleton_produces_identity() {
    let pose = HashMap::<String, JointPose>::new();
    let skeleton = make_single_joint_skeleton("mRoot");
    let skin_data = make_skin_data("nonexistent");
    let matrices =
        AnimeshSkinning::compute_skinning_matrices(Some(pose), Some(skeleton), Some(skin_data))
            .expect("ComputeSkinningMatrices");
    assert_eq!(matrices.len(), 1);
    assert_matrix_identity(matrices[0], 0.0);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.ComputeSkinningMatrices_IdentitySkeletonAndBindPose_ProducesIdentityMatrix::test 78e0605212c88c8ae33dd68ebe8a3ece75627761f81225b188801dc7ef9e5b32 translated
#[test]
fn animesh_skinning_compute_matrices_identity_skeleton_and_bind_pose_produces_identity() {
    let pose = HashMap::<String, JointPose>::new();
    let skeleton = make_single_joint_skeleton("mRoot");
    let skin_data = make_skin_data("mRoot");
    let matrices =
        AnimeshSkinning::compute_skinning_matrices(Some(pose), Some(skeleton), Some(skin_data))
            .expect("ComputeSkinningMatrices");
    assert_eq!(matrices.len(), 1);
    assert_matrix_identity(matrices[0], 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.DeformVertices_NullWeights_PassesPositionsThrough::test bbeb45d84770e6b766272bf9a5d48bd415ba98681d9da70d256598429566144d translated
#[test]
fn animesh_skinning_deform_vertices_null_weights_passes_positions_through() {
    let face = face(
        vec![
            vertex(vector3(1.0, 2.0, 3.0)),
            vertex(vector3(4.0, 5.0, 6.0)),
        ],
        None,
    );
    let mut positions = [Vector3::zero(), Vector3::zero()];
    AnimeshSkinning::deform_vertices(face, Vec::new(), Matrix4::identity(), &mut positions, None)
        .expect("DeformVertices");
    assert_eq!(
        (positions[0].x, positions[0].y, positions[0].z),
        (1.0, 2.0, 3.0)
    );
    assert_eq!(
        (positions[1].x, positions[1].y, positions[1].z),
        (4.0, 5.0, 6.0)
    );
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.DeformVertices_ShortOutputSpan_ThrowsArgumentException::test dfb10b33cedddadbaa5431f1fdf8a1c59369441e94618f97ef13f53ab931907b translated
#[test]
fn animesh_skinning_deform_vertices_short_output_span_throws_argument() {
    let face = face(vec![vertex(Vector3::zero()), vertex(Vector3::zero())], None);
    let mut positions = [Vector3::zero()];
    let Err(error) = AnimeshSkinning::deform_vertices(
        face,
        Vec::new(),
        Matrix4::identity(),
        &mut positions,
        None,
    ) else {
        panic!("short output span must fail");
    };
    assert_eq!(error, Error::Argument);
}

// parity-case: LibreMetaverse.Tests/AnimeshSkinningTests.cs::AnimeshSkinningTests.DeformVertices_IdentitySkinning_FullWeight_PreservesPositions::test fcf305a633047e7a4181954fee4990db3537e549f2cb940b08e6d3f0f544c8e1 translated
#[test]
fn animesh_skinning_deform_vertices_identity_skinning_full_weight_preserves_positions() {
    let expected = vector3(3.0, 7.0, -2.0);
    let face = face(
        vec![vertex(expected)],
        Some(vec![VertexWeight {
            joint0: 0,
            joint1: 0,
            joint2: 0,
            joint3: 0,
            weight0: 1.0,
            weight1: 0.0,
            weight2: 0.0,
            weight3: 0.0,
        }]),
    );
    let mut positions = [Vector3::zero()];
    AnimeshSkinning::deform_vertices(
        face,
        vec![Matrix4::identity()],
        Matrix4::identity(),
        &mut positions,
        None,
    )
    .expect("DeformVertices");
    assert_close(positions[0].x, expected.x, 1.0e-5);
    assert_close(positions[0].y, expected.y, 1.0e-5);
    assert_close(positions[0].z, expected.z, 1.0e-5);
}

// parity-case: LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.RequestOwnAvatarTextures_WhenNotConnected_DoesNotThrow::test bf3539f68a0adc58c2166289ca1c75f6ff03906f71ffe2fb749f6990404b541a translated
#[test]
fn avatar_manager_request_own_avatar_textures_when_not_connected_does_not_throw() {
    let client = GridClient::new().expect("GridClient constructor");
    client
        .avatars()
        .request_own_avatar_textures()
        .expect("RequestOwnAvatarTextures");
}

// parity-case: LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.UpdateLastReceivedCOFVersion_AcceptsHigherVersion::test 3656364491dcd7a05c868b11ea0d8be48609d10c6054b9696f54cbac10cfe6df translated
#[test]
fn appearance_manager_update_last_received_cof_version_accepts_higher_version() {
    let client = GridClient::new().expect("GridClient constructor");
    client
        .appearance()
        .update_last_received_cof_version(3)
        .expect("UpdateLastReceivedCOFVersion");
    client
        .appearance()
        .update_last_received_cof_version(8)
        .expect("UpdateLastReceivedCOFVersion");
    assert_eq!(client.appearance().last_update_received_cof_version(), 8);
}

// parity-case: LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.UpdateLastReceivedCOFVersion_RejectsLowerVersion::test f4a827fcace9d50306a8c375183e056140996d2a39b02e4dd12fc6cc79e5d0f0 translated
#[test]
fn appearance_manager_update_last_received_cof_version_rejects_lower_version() {
    let client = GridClient::new().expect("GridClient constructor");
    client
        .appearance()
        .update_last_received_cof_version(10)
        .expect("UpdateLastReceivedCOFVersion");
    client
        .appearance()
        .update_last_received_cof_version(4)
        .expect("UpdateLastReceivedCOFVersion");
    assert_eq!(client.appearance().last_update_received_cof_version(), 10);
}

// parity-case: LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.UpdateLastReceivedCOFVersion_EqualVersion_IsNotAdvanced::test e3db5962cdad5e18ad27a36d0d7cca2d471e96df7513315d0f34bb0db99d723c translated
#[test]
fn appearance_manager_update_last_received_cof_version_equal_version_is_not_advanced() {
    let client = GridClient::new().expect("GridClient constructor");
    client
        .appearance()
        .update_last_received_cof_version(6)
        .expect("UpdateLastReceivedCOFVersion");
    client
        .appearance()
        .update_last_received_cof_version(6)
        .expect("UpdateLastReceivedCOFVersion");
    assert_eq!(client.appearance().last_update_received_cof_version(), 6);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_EmptyFilter_ReturnsEmptyList::test 59857e0c12d374a3063e98311f7d262ca9e4fe3bde43af4698d20a6e6e550a64 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_empty_filter_returns_empty_list() {
    let skeleton = make_skeleton(
        "mPelvis",
        vec![make_joint("mTorso", vec![make_joint("mChest", Vec::new())])],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(Vec::<String>::new().into_iter()))
        .expect("BuildExpandedJointList");
    assert!(result.is_empty());
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_NoBoneChildren_ReturnsEmptyList::test c744ad17de5b5dd0e594973181307ddaba0d9b0ecf0ce3ff258e108de45e82be translated
#[test]
fn linden_skeleton_build_expanded_joint_list_no_bone_children_returns_empty_list() {
    let mut root = Joint::new().expect("Joint constructor");
    root.base.set_name("mPelvis".to_owned());
    root.set_bone(None);
    let mut skeleton = LindenSkeleton::new().expect("LindenSkeleton constructor");
    skeleton.set_bone(root);
    let result = skeleton
        .build_expanded_joint_list(Box::new(vec!["mPelvis".to_owned()].into_iter()))
        .expect("BuildExpandedJointList");
    assert!(result.is_empty());
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_LinearChain_AllInFilter_ProducesCorrectSequence::test c2fa9fc02df25cb6eb083746a100abf551d208ea036cadb26a1e94649d33e560 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_linear_chain_all_in_filter() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "A",
            vec![make_joint("B", vec![make_joint("C", Vec::new())])],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(["A", "B", "C"].map(str::to_owned).into_iter()))
        .expect("BuildExpandedJointList");
    assert_eq!(result, vec!["A", "B", "C"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_IntermediateBoneNotInFilter_IsSkippedAsPlaceholder::test 872b6dae196f3700d62b44b6fc3ec50cd41ca78cf36fa3f63d5967b6e1e47bce translated
#[test]
fn linden_skeleton_build_expanded_joint_list_intermediate_not_in_filter_is_skipped() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "A",
            vec![make_joint("B", vec![make_joint("C", Vec::new())])],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(["A", "C"].map(str::to_owned).into_iter()))
        .expect("BuildExpandedJointList");
    assert!(!result.contains(&"B".to_owned()));
    assert_eq!(result, vec!["A", "C"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_MultipleConsecutiveIntermediates_AllSkipped::test 033ff727d7f4585321f81b2e4584395bdcbf22ad89eac601be67a1887735f944 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_multiple_intermediates_all_skipped() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "A",
            vec![make_joint(
                "inter1",
                vec![make_joint("inter2", vec![make_joint("B", Vec::new())])],
            )],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(["A", "B"].map(str::to_owned).into_iter()))
        .expect("BuildExpandedJointList");
    assert!(!result.contains(&"inter1".to_owned()));
    assert!(!result.contains(&"inter2".to_owned()));
    assert_eq!(result, vec!["A", "B"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_BranchingHierarchy_BranchPointRepeatedPerChild::test ff01368c80870ec7d9b4e5eed52bbb3254e8fbf0c06d09f245abf2a450eae6c6 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_branch_point_repeated_per_child() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "A",
            vec![
                make_joint("B", vec![make_joint("C", Vec::new())]),
                make_joint("D", Vec::new()),
            ],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(
            ["A", "B", "C", "D"].map(str::to_owned).into_iter(),
        ))
        .expect("BuildExpandedJointList");
    assert_eq!(result, vec!["A", "B", "C", "A", "D"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_BranchingWithIntermediates_EffectiveParentUsedForAllBranches::test 5145f5db024a09f833a903f992f42ecc827973f7eddae31a7164e91df0b03ab3 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_branching_uses_effective_parent() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "A",
            vec![
                make_joint("B", vec![make_joint("C", Vec::new())]),
                make_joint("D", Vec::new()),
            ],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(["A", "C", "D"].map(str::to_owned).into_iter()))
        .expect("BuildExpandedJointList");
    assert!(!result.contains(&"B".to_owned()));
    assert_eq!(result, vec!["A", "C", "A", "D"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_OnlyLeafInFilter_ReachableViaRootPlaceholder::test e4b894c6b0479350e3ef57aab2ccb8d720a7d37df02e09ad4317953804f098fe translated
#[test]
fn linden_skeleton_build_expanded_joint_list_only_leaf_in_filter() {
    let skeleton = make_skeleton(
        "root",
        vec![make_joint(
            "inter1",
            vec![make_joint("inter2", vec![make_joint("leaf", Vec::new())])],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(vec!["leaf".to_owned()].into_iter()))
        .expect("BuildExpandedJointList");
    assert_eq!(result, vec!["leaf"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_SingleFilteredDirectChild_ProducesTwoEntries::test f9191747ebd65cf4bbe5b7780400a526f26db86545a08e581348b78f2bf61d18 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_single_filtered_direct_child() {
    let skeleton = make_skeleton("root", vec![make_joint("A", Vec::new())]);
    let result = skeleton
        .build_expanded_joint_list(Box::new(vec!["A".to_owned()].into_iter()))
        .expect("BuildExpandedJointList");
    assert_eq!(result, vec!["A"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildExpandedJointList_SpineBonesSkipped_DownstreamIndicesNotShifted::test 779aefe6b9fb007f17fed5d2bb6da5d81cf908d938fdceff7c9b155747d5dad9 translated
#[test]
fn linden_skeleton_build_expanded_joint_list_spine_bones_skipped() {
    let skeleton = make_skeleton(
        "mPelvis",
        vec![make_joint(
            "mTorso",
            vec![make_joint(
                "mSpine1",
                vec![make_joint(
                    "mChest",
                    vec![make_joint("mNeck", vec![make_joint("mHead", Vec::new())])],
                )],
            )],
        )],
    );
    let result = skeleton
        .build_expanded_joint_list(Box::new(
            ["mTorso", "mChest", "mHead"].map(str::to_owned).into_iter(),
        ))
        .expect("BuildExpandedJointList");
    assert!(!result.contains(&"mSpine1".to_owned()));
    assert!(!result.contains(&"mNeck".to_owned()));
    assert_eq!(result, vec!["mTorso", "mChest", "mHead"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.JointBase_SupportCategory_ReturnsBase_WhenAttributeIsBase::test 809bde18eab918a7ba51c1af1659718ec7f3a7ffc91c3dd8d462b298c78ce968 translated
#[test]
fn joint_base_support_category_returns_base_when_attribute_is_base() {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name("mTorso".to_owned());
    joint.base.set_support("base".to_owned());
    assert_eq!(joint.base.support_category(), JointSupportCategory::Base);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.JointBase_SupportCategory_ReturnsExtended_WhenAttributeIsExtended::test 47c16fd9e292b4d5de4e28f3b932ef8f7e04e087fd1a0023db3666e85e28762b translated
#[test]
fn joint_base_support_category_returns_extended_when_attribute_is_extended() {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name("mTail1".to_owned());
    joint.base.set_support("extended".to_owned());
    assert_eq!(
        joint.base.support_category(),
        JointSupportCategory::Extended
    );
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.JointBase_SupportCategory_DefaultsToBase_WhenAttributeIsMissing::test 4e5faf4477a615488ac257c511ec1a02c8f53f64e34260fd48af297807dae723 translated
#[test]
fn joint_base_support_category_defaults_to_base_when_attribute_is_missing() {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name("mTest".to_owned());
    assert_eq!(joint.base.support_category(), JointSupportCategory::Base);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.Joint_GetAliasesList_ReturnsSplitAliases_WhenAttributeIsSet::test b28dd565d89d53fd1e5f75fa0cc4956661e9ceab65a30290c60d72f619a5e533 translated
#[test]
fn joint_get_aliases_list_returns_split_aliases_when_attribute_is_set() {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name("mPelvis".to_owned());
    joint.set_aliases("hip avatar_mPelvis".to_owned());
    assert_eq!(
        joint.get_aliases_list().expect("GetAliasesList"),
        vec!["hip", "avatar_mPelvis"]
    );
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.Joint_GetAliasesList_ReturnsEmpty_WhenAttributeIsEmpty::test fdab4aa0aafa3739c96c673e974ad89cbdaca458c8f1c0e12a38dd6074b9850a translated
#[test]
fn joint_get_aliases_list_returns_empty_when_attribute_is_empty() {
    let mut joint = Joint::new().expect("Joint constructor");
    joint.base.set_name("mHead".to_owned());
    assert!(joint.get_aliases_list().expect("GetAliasesList").is_empty());
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.GetAllJoints_ReturnsAllJointsDepthFirst::test 93b9b834a8f87acb434c927512fd355e171890afe7a50499f6bed1f7c020d218 translated
#[test]
fn linden_skeleton_get_all_joints_returns_all_joints_depth_first() {
    let skeleton = make_skeleton(
        "root",
        vec![
            make_joint("A", vec![make_joint("B", Vec::new())]),
            make_joint("C", Vec::new()),
        ],
    );
    let names: Vec<String> = skeleton
        .get_all_joints()
        .expect("GetAllJoints")
        .map(|joint| joint.base.name())
        .collect();
    assert_eq!(names, vec!["root", "A", "B", "C"]);
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.BuildJointDictionary_ContainsCanonicalNamesAndAliases::test d9fd2e8103d19e2c399b0e8a4dd11db2dfd0290bffd678253f64f3c07bd98c24 translated
#[test]
fn linden_skeleton_build_joint_dictionary_contains_names_and_aliases() {
    let mut child = Joint::new().expect("Joint constructor");
    child.base.set_name("mPelvis".to_owned());
    child.set_aliases("hip avatar_mPelvis".to_owned());
    let skeleton = make_skeleton("root", vec![child]);
    let dictionary = skeleton
        .build_joint_dictionary()
        .expect("BuildJointDictionary");
    assert!(dictionary.contains_key("root"));
    assert!(dictionary.contains_key("mPelvis"));
    assert!(dictionary.contains_key("hip"));
    assert!(dictionary.contains_key("avatar_mPelvis"));
    assert!(Arc::ptr_eq(
        dictionary.get("hip").expect("hip alias"),
        dictionary.get("mPelvis").expect("canonical name")
    ));
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.GetBone_ReturnsBone_ByCanonicalName::test 87761df2ed95f6dc61a89717a794956faca53a28092464c276406bd777e888a1 translated
#[test]
fn linden_skeleton_get_bone_returns_bone_by_canonical_name() {
    let skeleton = make_skeleton("root", vec![make_joint("mTorso", Vec::new())]);
    let joint = skeleton
        .get_bone("mTorso".to_owned())
        .expect("GetBone")
        .expect("mTorso joint");
    assert_eq!(joint.base.name(), "mTorso");
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.GetBone_ReturnsBone_ByAlias::test 2bad9d7c6ad2b2944537bbf30c89370957f4ce54785c797e0bf234b44aacce2a translated
#[test]
fn linden_skeleton_get_bone_returns_bone_by_alias() {
    let mut child = Joint::new().expect("Joint constructor");
    child.base.set_name("mPelvis".to_owned());
    child.set_aliases("hip".to_owned());
    let skeleton = make_skeleton("root", vec![child]);
    let joint = skeleton
        .get_bone("hip".to_owned())
        .expect("GetBone")
        .expect("hip alias");
    assert_eq!(joint.base.name(), "mPelvis");
}

// parity-case: LibreMetaverse.Tests/LindenSkeletonTests.cs::LindenSkeletonTests.GetBone_ReturnsNull_ForUnknownName::test 19b80d25352f1181a0ccb5e96d068840440dc9352e0e8760ee7774edd414451c translated
#[test]
fn linden_skeleton_get_bone_returns_none_for_unknown_name() {
    let skeleton = make_skeleton("root", vec![make_joint("mTorso", Vec::new())]);
    assert!(
        skeleton
            .get_bone("nonexistent".to_owned())
            .expect("GetBone")
            .is_none()
    );
}
