use std::f32::consts::{FRAC_PI_2, FRAC_PI_3};

use roofy::{Affine3, Mat3, Mat4, Quat, Vec3, Vec4};

#[test]
fn const_nan() {
    let a: [f32; 16] = Mat4::NAN.into();
    assert!(a.iter().all(|x| x.is_nan()))
}

#[test]
fn const_zero() {
    assert_eq!(
        Mat4::ZERO,
        Mat4::from_cols(Vec4::ZERO, Vec4::ZERO, Vec4::ZERO, Vec4::ZERO)
    );
}

#[test]
fn const_identity() {
    assert_eq!(
        Mat4::IDENTITY,
        Mat4::from_cols(Vec4::X, Vec4::Y, Vec4::Z, Vec4::W)
    );
}

#[test]
fn new() {
    assert_eq!(
        Mat4::new(
            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
        ),
        Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        )
    );
}

#[test]
fn from_cols() {
    assert_eq!(
        Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        ),
        Mat4 {
            col0: Vec4::new(1.0, 2.0, 3.0, 4.0),
            col1: Vec4::new(5.0, 6.0, 7.0, 8.0),
            col2: Vec4::new(9.0, 10.0, 11.0, 12.0),
            col3: Vec4::new(13.0, 14.0, 15.0, 16.0),
        }
    );
}

#[test]
fn from_diagonal() {
    assert_eq!(Mat4::from_diagonal(Vec4::ONE), Mat4::IDENTITY);
}

#[test]
fn from_array() {
    assert_eq!(
        Mat4::from_array([
            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
        ]),
        Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        )
    );
}

#[test]
fn to_array() {
    assert_eq!(
        Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        )
        .to_array(),
        [
            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
        ]
    );
}

#[test]
fn from_slice() {
    assert_eq!(
        Mat4::from_slice(&[
            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0
        ]),
        Mat4::IDENTITY
    );
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn from_slice_panics_if_too_short() {
    let _ = Mat4::from_slice(&[0.0; 15]);
}

#[test]
fn write_to_slice() {
    let mut slice: [f32; 16] = [0.0; 16];
    Mat4::IDENTITY.write_to_slice(&mut slice);
    assert_eq!(
        slice,
        [
            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0
        ]
    );
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn write_to_slice_panics_if_to_short() {
    let mut slice: [f32; 15] = [0.0; 15];
    Mat4::IDENTITY.write_to_slice(&mut slice);
}

#[test]
fn from_mat3() {
    assert_eq!(Mat4::from_mat3(Mat3::IDENTITY), Mat4::IDENTITY);
}

#[test]
fn from_mat3_translation() {
    assert_eq!(
        Mat4::from_mat3_translation(Mat3::IDENTITY, Vec3::ZERO),
        Mat4::IDENTITY
    );
}

#[test]
fn from_translation_zero_is_identity() {
    assert_eq!(Mat4::from_translation(Vec3::ZERO), Mat4::IDENTITY);
}

#[test]
fn from_translation() {
    let m: Mat4 = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));

    assert_eq!(m.col3, Vec4::new(1.0, 2.0, 3.0, 1.0));
}

#[test]
fn from_scale_one_is_identity() {
    assert_eq!(Mat4::from_scale(Vec3::ONE), Mat4::IDENTITY);
}

#[test]
fn from_scale_scales_components() {
    let m: Mat4 = Mat4::from_scale(Vec3::new(1.0, 2.0, 3.0));

    assert_eq!(m * Vec4::ONE, Vec4::new(1.0, 2.0, 3.0, 1.0));
}

#[test]
fn from_rotation_x() {
    let m: Mat4 = Mat4::from_rotation_x(FRAC_PI_2);
    let v: Vec4 = m * Vec4::Y;

    assert!(v.approx_eq(Vec4::Z));
}

#[test]
fn from_rotation_x_does_not_affect_x_axis() {
    let m: Mat4 = Mat4::from_rotation_x(FRAC_PI_2);

    assert_eq!(m * Vec4::X, Vec4::X);
}

#[test]
fn from_rotation_x_degrees_matches() {
    assert_eq!(
        Mat4::from_rotation_x_degrees(90.0),
        Mat4::from_rotation_x(FRAC_PI_2)
    );
}

#[test]
fn from_rotation_y() {
    let m: Mat4 = Mat4::from_rotation_y(FRAC_PI_2);
    let v: Vec4 = m * Vec4::Z;

    assert!(v.approx_eq(Vec4::X));
}

#[test]
fn from_rotation_y_does_not_affect_y_axis() {
    let m: Mat4 = Mat4::from_rotation_y(FRAC_PI_2);

    assert_eq!(m * Vec4::Y, Vec4::Y);
}

#[test]
fn from_rotation_y_degrees_matches() {
    assert_eq!(
        Mat4::from_rotation_y_degrees(90.0),
        Mat4::from_rotation_y(FRAC_PI_2)
    );
}

#[test]
fn from_rotation_z() {
    let m: Mat4 = Mat4::from_rotation_z(FRAC_PI_2);
    let v: Vec4 = m * Vec4::X;

    assert!(v.approx_eq(Vec4::Y));
}

#[test]
fn from_rotation_z_does_not_affect_z_axis() {
    let m: Mat4 = Mat4::from_rotation_z(FRAC_PI_2);

    assert_eq!(m * Vec4::Z, Vec4::Z);
}

#[test]
fn from_rotation_z_degrees_matches() {
    assert_eq!(
        Mat4::from_rotation_z_degrees(90.0),
        Mat4::from_rotation_z(FRAC_PI_2)
    );
}

#[test]
fn from_trs_euler_identity_components_is_identity() {
    assert_eq!(
        Mat4::from_trs_euler(Vec3::ZERO, Vec3::ZERO, Vec3::ONE),
        Mat4::IDENTITY
    );
}

#[test]
fn from_trs_euler_translation() {
    assert_eq!(
        Mat4::from_trs_euler(Vec3::new(1.0, 2.0, 3.0), Vec3::ZERO, Vec3::ONE),
        Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0))
    );
}

#[test]
fn from_trs_euler_scale() {
    assert_eq!(
        Mat4::from_trs_euler(Vec3::ZERO, Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0)),
        Mat4::from_scale(Vec3::new(1.0, 2.0, 3.0))
    );
}

#[test]
fn from_trs_euler_scale_then_translate() {
    let m: Mat4 = Mat4::from_trs_euler(Vec3::new(10.0, 0.0, 0.0), Vec3::ZERO, Vec3::splat(2.0));
    let v: Vec4 = Vec4::new(1.0, 0.0, 0.0, 1.0);

    assert_eq!(m * v, Vec4::new(12.0, 0.0, 0.0, 1.0));
}

#[test]
fn from_trs_euler_degrees_matches_from_trs_euler() {
    assert_eq!(
        Mat4::from_trs_euler_degrees(Vec3::ZERO, Vec3::splat(90.0), Vec3::ZERO),
        Mat4::from_trs_euler(Vec3::ZERO, Vec3::splat(FRAC_PI_2), Vec3::ZERO)
    );
}

#[test]
fn from_trs_rotation_matches_from_quat() {
    let q: Quat = Quat::from_axis_angle(Vec3::Y, FRAC_PI_2);
    let via_trs: Vec3 = Mat4::from_trs(Vec3::ZERO, q, Vec3::ONE).transform_vector(Vec3::X);
    let via_quat: Vec3 = q * Vec3::X;

    assert_eq!(via_trs, via_quat);
}

#[test]
fn from_quat_identity_is_identity() {
    assert_eq!(Mat4::from_quat(Quat::IDENTITY), Mat4::IDENTITY);
}

#[test]
fn from_quat_matches_rotation_x() {
    assert!(
        Mat4::from_quat(Quat::from_axis_angle(Vec3::X, FRAC_PI_2))
            .approx_eq(Mat4::from_rotation_x(FRAC_PI_2))
    );
}

#[test]
fn from_quat_matches_rotation_y() {
    assert!(
        Mat4::from_quat(Quat::from_axis_angle(Vec3::Y, FRAC_PI_2))
            .approx_eq(Mat4::from_rotation_y(FRAC_PI_2))
    );
}

#[test]
fn from_quat_matches_rotation_z() {
    assert!(
        Mat4::from_quat(Quat::from_axis_angle(Vec3::Z, FRAC_PI_2))
            .approx_eq(Mat4::from_rotation_z(FRAC_PI_2))
    );
}

#[test]
fn from_quat_preserves_vector_length() {
    let q: Quat = Quat::from_axis_angle(Vec3::new(1.0, 1.0, 0.0).normalized(), 1.23);
    let v: Vec3 = Vec3::new(3.0, 4.0, 0.0);
    let r: Vec3 = Mat4::from_quat(q).transform_vector(v);

    assert!((r.length_squared() - 25.0).abs() < 1e-5);
}

#[test]
fn from_axis_angle_zero_is_identity() {
    assert_eq!(Mat4::from_axis_angle(Vec3::Y, 0.0), Mat4::IDENTITY);
}

#[test]
fn from_axis_angle_matches_rotation_x() {
    assert!(Mat4::from_axis_angle(Vec3::X, FRAC_PI_2).approx_eq(Mat4::from_rotation_x(FRAC_PI_2)));
}

#[test]
fn from_axis_angle_matches_rotation_y() {
    assert!(Mat4::from_axis_angle(Vec3::Y, FRAC_PI_2).approx_eq(Mat4::from_rotation_y(FRAC_PI_2)));
}

#[test]
fn from_axis_angle_matches_rotation_z() {
    assert!(Mat4::from_axis_angle(Vec3::Z, FRAC_PI_2).approx_eq(Mat4::from_rotation_z(FRAC_PI_2)));
}

#[test]
fn from_axis_angle_degrees_matches() {
    assert_eq!(
        Mat4::from_axis_angle_degrees(Vec3::X, 90.0),
        Mat4::from_axis_angle(Vec3::X, FRAC_PI_2)
    );
}

#[test]
fn look_to_rh_maps_eye_to_origin() {
    let eye: Vec3 = Vec3::new(0.0, 0.0, 5.0);
    let view: Mat4 = Mat4::look_to_rh(eye, Vec3::NEG_Z, Vec3::Y);

    assert_eq!(view.transform_point(eye), Vec3::ZERO);
}

#[test]
fn look_to_lh_maps_eye_to_origin() {
    let eye: Vec3 = Vec3::new(0.0, 0.0, -5.0);
    let view: Mat4 = Mat4::look_to_lh(eye, Vec3::Z, Vec3::Y);

    assert_eq!(view.transform_point(eye), Vec3::ZERO);
}

#[test]
fn look_to_rh_maps_forward_point_onto_negative_z() {
    let eye: Vec3 = Vec3::ZERO;
    let dir: Vec3 = Vec3::NEG_Z;
    let view: Mat4 = Mat4::look_to_rh(eye, dir, Vec3::Y);

    let transformed: Vec3 = view.transform_point(eye + dir);

    assert_eq!(transformed, Vec3::NEG_Z);
}

#[test]
fn look_to_lh_maps_forward_point_onto_positive_z() {
    let eye: Vec3 = Vec3::ZERO;
    let dir: Vec3 = Vec3::Z;
    let view: Mat4 = Mat4::look_to_lh(eye, dir, Vec3::Y);

    let transformed: Vec3 = view.transform_point(eye + dir);

    assert_eq!(transformed, Vec3::Z);
}

#[test]
fn look_at_rh_matches_look_to_rh() {
    let eye: Vec3 = Vec3::new(1.0, 2.0, 3.0);
    let center: Vec3 = Vec3::new(-1.0, 0.0, 1.0);
    let up: Vec3 = Vec3::Y;

    assert_eq!(
        Mat4::look_at_rh(eye, center, up),
        Mat4::look_to_rh(eye, (center - eye).normalized(), up)
    );
}

#[test]
fn look_at_lh_matches_look_to_rh() {
    let eye: Vec3 = Vec3::new(1.0, 2.0, 3.0);
    let center: Vec3 = Vec3::new(-1.0, 0.0, 1.0);
    let up: Vec3 = Vec3::Y;

    assert_eq!(
        Mat4::look_at_lh(eye, center, up),
        Mat4::look_to_lh(eye, (center - eye).normalized(), up)
    );
}

#[test]
fn look_at_lh_looking_down_z() {
    let m: Mat4 = Mat4::look_at_lh(Vec3::NEG_Z, Vec3::ZERO, Vec3::Y);
    let v: Vec4 = m * Vec4::W;

    assert!(v.z > 0.0);
}

#[test]
fn perspective_rh_x_scales_with_aspect_ratio() {
    let aspect: f32 = 2.0;
    let m: Mat4 = Mat4::perspective_rh(FRAC_PI_2, aspect, 0.1, 100.0);
    let v: Vec4 = m * Vec4::new(1.0, 0.0, -1.0, 1.0);
    let expected_x: f32 = 1.0 / aspect;
    assert!((v.x / v.w - expected_x).abs() < 1e-5);
}

#[test]
fn perspective_rh_y_independent_of_aspect_ratio() {
    let aspect: f32 = 2.0;
    let m: Mat4 = Mat4::perspective_rh(FRAC_PI_2, aspect, 0.1, 100.0);
    let v: Vec4 = m * Vec4::new(0.0, 1.0, -1.0, 1.0);
    assert!((v.y / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_rh_fov_changes_y_scale() {
    let fov: f32 = FRAC_PI_3;
    let m: Mat4 = Mat4::perspective_rh(fov, 1.0, 0.1, 100.0);
    let v: Vec4 = m * Vec4::new(0.0, 1.0, -1.0, 1.0);
    let expected_f: f32 = 1.0 / (fov * 0.5).tan();
    assert!((v.y / v.w - expected_f).abs() < 1e-5);
}

#[test]
fn perspective_lh_x_scales_with_aspect_ratio() {
    let aspect: f32 = 2.0;
    let m: Mat4 = Mat4::perspective_lh(FRAC_PI_2, aspect, 0.1, 100.0);
    let v: Vec4 = m * Vec4::new(1.0, 0.0, 1.0, 1.0);
    let expected_x: f32 = 1.0 / aspect;
    assert!((v.x / v.w - expected_x).abs() < 1e-5);
}

#[test]
fn perspective_lh_y_independent_of_aspect_ratio() {
    let aspect: f32 = 2.0;
    let m: Mat4 = Mat4::perspective_lh(FRAC_PI_2, aspect, 0.1, 100.0);
    let v: Vec4 = m * Vec4::new(0.0, 1.0, 1.0, 1.0);
    assert!((v.y / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_rh_near_maps_to_zero_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_rh(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_near, 1.0);
    assert!((v.z / v.w).abs() < 1e-5);
}

#[test]
fn perspective_rh_far_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_rh(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_far, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_lh_near_maps_to_zero_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_lh(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_near, 1.0);
    assert!((v.z / v.w).abs() < 1e-5);
}

#[test]
fn perspective_lh_far_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_lh(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_far, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_rh_no_near_maps_to_neg_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_rh_no(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_near, 1.0);
    assert!((v.z / v.w + 1.0).abs() < 1e-5);
}

#[test]
fn perspective_rh_no_far_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_rh_no(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_far, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_lh_no_near_maps_to_neg_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_lh_no(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_near, 1.0);
    assert!((v.z / v.w + 1.0).abs() < 1e-5);
}

#[test]
fn perspective_lh_no_far_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let z_far: f32 = 100.0;
    let m: Mat4 = Mat4::perspective_lh_no(FRAC_PI_2, 1.0, z_near, z_far);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_far, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}

#[test]
fn perspective_infinite_rh_x_scales_with_aspect_ratio() {
    let aspect: f32 = 2.0;
    let m: Mat4 = Mat4::perspective_infinite_rh(FRAC_PI_2, aspect, 0.1);
    let v: Vec4 = m * Vec4::new(1.0, 0.0, -1.0, 1.0);
    let expected_x: f32 = 1.0 / aspect;
    assert!((v.x / v.w - expected_x).abs() < 1e-5);
}

#[test]
fn perspective_infinite_rh_near_maps_to_zero_depth() {
    let z_near: f32 = 0.1;
    let m: Mat4 = Mat4::perspective_infinite_rh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_near, 1.0);
    assert!((v.z / v.w).abs() < 1e-5);
}
#[test]
fn perspective_infinite_rh_far_approaches_one_depth() {
    let z_near: f32 = 0.1;
    let z_far_approx: f32 = 1e6;
    let m: Mat4 = Mat4::perspective_infinite_rh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_far_approx, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-4);
}

#[test]
fn perspective_infinite_lh_near_maps_to_zero_depth() {
    let z_near: f32 = 0.1;
    let m: Mat4 = Mat4::perspective_infinite_lh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_near, 1.0);
    assert!((v.z / v.w).abs() < 1e-5);
}
#[test]
fn perspective_infinite_lh_far_approaches_one_depth() {
    let z_near: f32 = 0.1;
    let z_far_approx: f32 = 1e6;
    let m: Mat4 = Mat4::perspective_infinite_lh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_far_approx, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-4);
}

#[test]
fn perspective_infinite_reverse_rh_near_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let m: Mat4 = Mat4::perspective_infinite_reverse_rh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_near, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}
#[test]
fn perspective_infinite_reverse_rh_far_approaches_zero_depth() {
    let z_near: f32 = 0.1;
    let z_far_approx: f32 = 1e6;
    let m: Mat4 = Mat4::perspective_infinite_reverse_rh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, -z_far_approx, 1.0);
    assert!((v.z / v.w).abs() < 1e-4);
}

#[test]
fn perspective_infinite_reverse_lh_near_maps_to_one_depth() {
    let z_near: f32 = 0.1;
    let m: Mat4 = Mat4::perspective_infinite_reverse_lh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_near, 1.0);
    assert!((v.z / v.w - 1.0).abs() < 1e-5);
}
#[test]
fn perspective_infinite_reverse_lh_far_approaches_zero_depth() {
    let z_near: f32 = 0.1;
    let z_far_approx: f32 = 1e6;
    let m: Mat4 = Mat4::perspective_infinite_reverse_lh(FRAC_PI_2, 1.0, z_near);
    let v: Vec4 = m * Vec4::new(0.0, 0.0, z_far_approx, 1.0);
    assert!((v.z / v.w).abs() < 1e-4);
}

#[test]
fn orthographic_rh_maps_center_xy_to_zero() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, -1.0, 1.0);

    assert_eq!(p.x, 0.0);
    assert_eq!(p.y, 0.0);
}

#[test]
fn orthographic_rh_preserves_w() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(1.0, 2.0, -50.0, 1.0);
    assert_eq!(p.w, 1.0);
}

#[test]
fn orthographic_rh_maps_right_edge_to_one() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(4.0, 0.0, -1.0, 1.0);

    assert_eq!(p.x, 1.0);
}

#[test]
fn orthographic_rh_maps_left_edge_to_neg_one() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(-4.0, 0.0, -1.0, 1.0);
    assert_eq!(p.x, -1.0);
}

#[test]
fn orthographic_rh_maps_top_edge_to_one() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 3.0, -1.0, 1.0);
    assert_eq!(p.y, 1.0);
}

#[test]
fn orthographic_rh_maps_bottom_edge_to_neg_one() {
    let m: Mat4 = Mat4::orthographic_rh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, -3.0, -1.0, 1.0);
    assert_eq!(p.y, -1.0);
}

#[test]
fn orthographic_rh_handles_asymmetric_bounds() {
    let m: Mat4 = Mat4::orthographic_rh(-1.0, 3.0, -2.0, 6.0, 0.1, 100.0);
    let center_x: f32 = (-1.0 + 3.0) / 2.0;
    let center_y: f32 = (-2.0 + 6.0) / 2.0;
    let p: Vec4 = m * Vec4::new(center_x, center_y, -1.0, 1.0);
    assert!(p.x.abs() < 1e-5);
    assert!(p.y.abs() < 1e-5);
}

#[test]
fn orthographic_lh_maps_xy_edges_correctly() {
    let m: Mat4 = Mat4::orthographic_lh(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let right: Vec4 = m * Vec4::new(4.0, 0.0, 0.1, 1.0);
    let left: Vec4 = m * Vec4::new(-4.0, 0.0, 0.1, 1.0);
    let top: Vec4 = m * Vec4::new(0.0, 3.0, 0.1, 1.0);
    let bottom: Vec4 = m * Vec4::new(0.0, -3.0, 0.1, 1.0);
    assert_eq!(right.x, 1.0);
    assert_eq!(left.x, -1.0);
    assert_eq!(top.y, 1.0);
    assert_eq!(bottom.y, -1.0);
}

#[test]
fn orthographic_rh_no_maps_xy_edges_correctly() {
    let m: Mat4 = Mat4::orthographic_rh_no(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let right: Vec4 = m * Vec4::new(4.0, 0.0, -1.0, 1.0);
    let top: Vec4 = m * Vec4::new(0.0, 3.0, -1.0, 1.0);
    assert_eq!(right.x, 1.0);
    assert_eq!(top.y, 1.0);
}

#[test]
fn orthographic_lh_no_maps_xy_edges_correctly() {
    let m: Mat4 = Mat4::orthographic_lh_no(-4.0, 4.0, -3.0, 3.0, 0.1, 100.0);
    let right: Vec4 = m * Vec4::new(4.0, 0.0, 0.1, 1.0);
    let top: Vec4 = m * Vec4::new(0.0, 3.0, 0.1, 1.0);
    assert_eq!(right.x, 1.0);
    assert_eq!(top.y, 1.0);
}

#[test]
fn orthographic_rh_maps_near_to_zero() {
    let m: Mat4 = Mat4::orthographic_rh(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, -0.1, 1.0);

    assert_eq!(p.z, 0.0);
}

#[test]
fn orthographic_rh_maps_far_to_one() {
    let m: Mat4 = Mat4::orthographic_rh(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, -100.0, 1.0);

    assert_eq!(p.z, 1.0);
}

#[test]
fn orthographic_lh_maps_near_to_zero() {
    let m: Mat4 = Mat4::orthographic_lh(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, 0.1, 1.0);

    assert_eq!(p.z, 0.0);
}

#[test]
fn orthographic_lh_maps_far_to_one() {
    let m: Mat4 = Mat4::orthographic_lh(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, 100.0, 1.0);

    assert_eq!(p.z, 1.0);
}

#[test]
fn orthographic_rh_no_maps_near_to_neg_one() {
    let m: Mat4 = Mat4::orthographic_rh_no(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, -0.1, 1.0);

    assert_eq!(p.z, -1.0);
}

#[test]
fn orthographic_rh_no_maps_far_to_one() {
    let m: Mat4 = Mat4::orthographic_rh_no(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, -100.0, 1.0);

    assert_eq!(p.z, 1.0);
}

#[test]
fn orthographic_lh_no_maps_near_to_neg_one() {
    let m: Mat4 = Mat4::orthographic_lh_no(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, 0.1, 1.0);

    assert_eq!(p.z, -1.0);
}

#[test]
fn orthographic_lh_no_maps_far_to_one() {
    let m: Mat4 = Mat4::orthographic_lh_no(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
    let p: Vec4 = m * Vec4::new(0.0, 0.0, 100.0, 1.0);

    assert_eq!(p.z, 1.0);
}

#[test]
fn cols() {
    assert_eq!(Mat4::IDENTITY.cols(), [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W]);
}

#[test]
fn col_matches() {
    assert_eq!(Mat4::IDENTITY.col(0), Mat4::IDENTITY.col0);
    assert_eq!(Mat4::IDENTITY.col(1), Mat4::IDENTITY.col1);
    assert_eq!(Mat4::IDENTITY.col(2), Mat4::IDENTITY.col2);
    assert_eq!(Mat4::IDENTITY.col(3), Mat4::IDENTITY.col3);
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn col_panics_out_of_bounds() {
    let _ = Mat4::IDENTITY.col(4);
}

#[test]
fn row0() {
    assert_eq!(Mat4::IDENTITY.row0(), Vec4::X);
}

#[test]
fn row1() {
    assert_eq!(Mat4::IDENTITY.row1(), Vec4::Y);
}

#[test]
fn row2() {
    assert_eq!(Mat4::IDENTITY.row2(), Vec4::Z);
}

#[test]
fn row3() {
    assert_eq!(Mat4::IDENTITY.row3(), Vec4::W);
}

#[test]
fn rows() {
    assert_eq!(Mat4::IDENTITY.rows(), [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W]);
}

#[test]
fn row_matches() {
    assert_eq!(Mat4::IDENTITY.row(0), Mat4::IDENTITY.row0());
    assert_eq!(Mat4::IDENTITY.row(1), Mat4::IDENTITY.row1());
    assert_eq!(Mat4::IDENTITY.row(2), Mat4::IDENTITY.row2());
    assert_eq!(Mat4::IDENTITY.row(3), Mat4::IDENTITY.row3());
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn row_panics_out_of_bounds() {
    let _ = Mat4::IDENTITY.row(4);
}

#[test]
fn translation_identity_is_zero() {
    assert_eq!(Mat4::IDENTITY.translation(), Vec3::ZERO);
}

#[test]
fn translation_roundtrip() {
    let t: Vec3 = Vec3::new(3.0, 5.0, 7.0);
    assert_eq!(Mat4::from_translation(t).translation(), t);
}

#[test]
fn transpose_identity() {
    assert_eq!(Mat4::IDENTITY.transpose(), Mat4::IDENTITY);
}

#[test]
fn transpose_swaps_rows_and_columns() {
    let m: Mat4 = Mat4::from_cols(
        Vec4::new(1.0, 2.0, 3.0, 4.0),
        Vec4::new(5.0, 6.0, 7.0, 8.0),
        Vec4::new(9.0, 10.0, 11.0, 12.0),
        Vec4::new(13.0, 14.0, 15.0, 16.0),
    );

    assert_eq!(
        m.transpose(),
        Mat4::from_cols(
            Vec4::new(1.0, 5.0, 9.0, 13.0),
            Vec4::new(2.0, 6.0, 10.0, 14.0),
            Vec4::new(3.0, 7.0, 11.0, 15.0),
            Vec4::new(4.0, 8.0, 12.0, 16.0)
        )
    )
}

#[test]
fn transpose_twice_stays_same() {
    let m: Mat4 = Mat4::from_cols(
        Vec4::new(1.0, 5.0, 9.0, 13.0),
        Vec4::new(2.0, 6.0, 10.0, 14.0),
        Vec4::new(3.0, 7.0, 11.0, 15.0),
        Vec4::new(4.0, 8.0, 12.0, 16.0),
    );

    assert_eq!(m.transpose().transpose(), m);
}

#[test]
fn determinant_zero_is_0() {
    assert_eq!(Mat4::ZERO.determinant(), 0.0);
}

#[test]
fn determinant_identity_is_1() {
    assert_eq!(Mat4::IDENTITY.determinant(), 1.0);
}

#[test]
fn determinant_duplicate_rows_is_zero() {
    assert_eq!(
        Mat4::from_cols(
            Vec4::new(1.0, 1.0, 0.0, 0.0),
            Vec4::new(2.0, 2.0, 0.0, 0.0),
            Vec4::new(3.0, 3.0, 1.0, 0.0),
            Vec4::new(4.0, 4.0, 0.0, 1.0)
        )
        .determinant(),
        0.0
    );
}

#[test]
fn determinant_is_product_of_diagonal() {
    assert_eq!(
        Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0)).determinant(),
        24.0
    );
}

#[test]
fn try_inverse_singular_is_none() {
    assert!(Mat4::ZERO.try_inverse().is_none());
}

#[test]
fn inverse_identity_is_identity() {
    assert_eq!(Mat4::IDENTITY.inverse(), Mat4::IDENTITY);
}

#[test]
fn inverse_times_original_is_identity() {
    let cases: [Mat4; 5] = [
        Mat4::IDENTITY,
        Mat4::from_scale(Vec3::new(2.0, 0.5, -3.0)),
        Mat4::from_translation(Vec3::new(1.0, -2.0, 3.5)),
        Mat4::from_rotation_y(1.0),
        Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0))
            * Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0))
            * Mat4::from_rotation_y(0.7),
    ];

    for m in cases {
        let id: Mat4 = m * m.inverse();
        assert!(id.approx_eq(Mat4::IDENTITY));
    }
}

#[test]
fn inverse_scale() {
    assert_eq!(
        Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0)).inverse(),
        Mat4::from_scale(Vec3::new(0.5, 1.0 / 3.0, 1.0 / 4.0))
    );
}

#[test]
fn inverse_translation() {
    assert_eq!(
        Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0)).inverse(),
        Mat4::from_translation(Vec3::new(-1.0, -2.0, -3.0))
    );
}

#[test]
fn inverse_rotation() {
    let m: Mat4 = Mat4::from_rotation_y(1.2);
    assert!(m.inverse().approx_eq(m.transpose()));
}

#[test]
#[should_panic(expected = "matrix not invertible")]
fn inverse_singular_panics() {
    let _ = Mat4::ZERO.inverse();
}

#[test]
fn inverse_composition_identity() {
    let a: Mat4 = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0))
        * Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0));

    let id: Mat4 = a * a.inverse();

    assert_eq!(id, Mat4::IDENTITY);
}

#[test]
fn is_invertible_identity() {
    assert!(Mat4::IDENTITY.is_invertible());
}

#[test]
fn is_invertible_zero_is_not_invertible() {
    assert!(!Mat4::ZERO.is_invertible());
}

#[test]
fn transform_point_identity_is_self() {
    let v: Vec3 = Vec3::new(1.0, 2.0, 3.0);

    assert_eq!(Mat4::IDENTITY.transform_point(v), v);
}

#[test]
fn transform_point_applies_translation() {
    let m: Mat4 = Mat4::from_translation(Vec3::new(10.0, 20.0, 30.0));
    let r: Vec3 = m.transform_point(Vec3::new(1.0, 2.0, 3.0));

    assert_eq!(r, Vec3::new(11.0, 22.0, 33.0));
}

#[test]
fn transform_point_applies_scale() {
    let m: Mat4 = Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0));
    let r: Vec3 = m.transform_point(Vec3::ONE);

    assert_eq!(r, Vec3::new(2.0, 3.0, 4.0));
}

#[test]
fn transform_direction_identity_is_self() {
    let v: Vec3 = Vec3::new(1.0, 2.0, 3.0);

    assert_eq!(Mat4::IDENTITY.transform_vector(v), v);
}

#[test]
fn transform_direction_ignores_translation() {
    let m: Mat4 = Mat4::from_translation(Vec3::new(10.0, 20.0, 30.0));
    let v: Vec3 = Vec3::new(1.0, 2.0, 3.0);

    assert_eq!(m.transform_vector(v), v);
}

#[test]
fn transform_direction_applies_scale() {
    let m: Mat4 = Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0));
    let r: Vec3 = m.transform_vector(Vec3::ONE);

    assert_eq!(r, Vec3::new(2.0, 3.0, 4.0));
}

#[test]
fn transform_direction_rotation_preserves_length() {
    let m: Mat4 = Mat4::from_rotation_y(FRAC_PI_2);
    let r: Vec3 = m.transform_vector(Vec3::X);

    assert_eq!(r.length_squared(), 1.0);
}

#[test]
fn abs() {
    assert_eq!((-Mat4::IDENTITY).abs(), Mat4::IDENTITY);
}

#[test]
fn recip() {
    assert_eq!(
        Mat4::IDENTITY.recip(),
        Mat4::from_cols(
            Vec4::X.recip(),
            Vec4::Y.recip(),
            Vec4::Z.recip(),
            Vec4::W.recip()
        )
    );
}

#[test]
fn approx_eq_identity_is_equal() {
    assert!(Mat4::IDENTITY.approx_eq(Mat4::IDENTITY));
}

#[test]
fn approx_eq_zero_is_not_identity() {
    assert!(!Mat4::ZERO.approx_eq(Mat4::IDENTITY));
}

#[test]
fn approx_eq_tiny_difference_is_equal() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m.col0.x += Mat4::EPSILON * 0.5;

    assert!(Mat4::IDENTITY.approx_eq(m));
}

#[test]
fn is_finite_identity_and_zero() {
    assert!(Mat4::IDENTITY.is_finite());
    assert!(Mat4::ZERO.is_finite());
}

#[test]
fn is_finite_nan() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m.col0.x = f32::NAN;

    assert!(!m.is_finite());
}

#[test]
fn is_finite_infinity() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m.col0.x = f32::INFINITY;

    assert!(!m.is_finite());
}

#[test]
fn is_finite_neg_infinity() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m.col0.x = f32::NEG_INFINITY;

    assert!(!m.is_finite());
}

#[test]
fn is_nan_zero_returns_false() {
    assert!(!Mat4::ZERO.is_nan());
}

#[test]
fn is_nan_nan_returns_true() {
    assert!(Mat4::NAN.is_nan());
}

#[test]
fn add() {
    assert_eq!(
        Mat4::IDENTITY + Mat4::IDENTITY,
        Mat4::from_diagonal(Vec4::splat(2.0))
    )
}

#[test]
fn add_assign() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m += Mat4::IDENTITY;
    assert_eq!(m, Mat4::from_diagonal(Vec4::splat(2.0)))
}

#[test]
fn sub() {
    assert_eq!(Mat4::IDENTITY - Mat4::IDENTITY, Mat4::ZERO)
}

#[test]
fn sub_assign() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m -= Mat4::IDENTITY;
    assert_eq!(m, Mat4::ZERO)
}

#[test]
fn mul_identity_is_noop() {
    let m: Mat4 = Mat4::from_cols(
        Vec4::new(1.0, 5.0, 9.0, 13.0),
        Vec4::new(2.0, 6.0, 10.0, 14.0),
        Vec4::new(3.0, 7.0, 11.0, 15.0),
        Vec4::new(4.0, 8.0, 12.0, 16.0),
    );

    assert_eq!(m * Mat4::IDENTITY, m);
    assert_eq!(Mat4::IDENTITY * m, m)
}

#[test]
fn mul_zero_returns_zero() {
    let m: Mat4 = Mat4::from_cols(
        Vec4::new(1.0, 5.0, 9.0, 13.0),
        Vec4::new(2.0, 6.0, 10.0, 14.0),
        Vec4::new(3.0, 7.0, 11.0, 15.0),
        Vec4::new(4.0, 8.0, 12.0, 16.0),
    );

    assert_eq!(m * Mat4::ZERO, Mat4::ZERO);
}

#[test]
fn mul_scalar_diagonal() {
    let a: Mat4 = Mat4::from_cols(
        Vec4::new(2.0, 0.0, 0.0, 0.0),
        Vec4::new(0.0, 2.0, 0.0, 0.0),
        Vec4::new(0.0, 0.0, 2.0, 0.0),
        Vec4::new(0.0, 0.0, 0.0, 2.0),
    );
    let b: Mat4 = Mat4::from_cols(
        Vec4::new(3.0, 0.0, 0.0, 0.0),
        Vec4::new(0.0, 3.0, 0.0, 0.0),
        Vec4::new(0.0, 0.0, 3.0, 0.0),
        Vec4::new(0.0, 0.0, 0.0, 3.0),
    );
    let expected: Mat4 = Mat4::from_cols(
        Vec4::new(6.0, 0.0, 0.0, 0.0),
        Vec4::new(0.0, 6.0, 0.0, 0.0),
        Vec4::new(0.0, 0.0, 6.0, 0.0),
        Vec4::new(0.0, 0.0, 0.0, 6.0),
    );

    assert_eq!(a * b, expected);
}

#[test]
fn mul_vec4_identity_stays_same() {
    let v: Vec4 = Vec4::new(1.0, 2.0, 3.0, 4.0);

    assert_eq!(Mat4::IDENTITY * v, v);
}

#[test]
fn mul_vec4_zero_returns_zero() {
    let v: Vec4 = Vec4::new(1.0, 2.0, 3.0, 4.0);

    assert_eq!(Mat4::ZERO * v, Vec4::ZERO);
}

#[test]
fn mul_f32() {
    assert_eq!(
        Mat4::IDENTITY * 2.0,
        Mat4::from_cols(Vec4::X * 2.0, Vec4::Y * 2.0, Vec4::Z * 2.0, Vec4::W * 2.0)
    );
}

#[test]
fn mul_assign_f32() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m *= 2.0;
    assert_eq!(m, Mat4::IDENTITY * 2.0);
}

#[test]
fn div() {
    assert_eq!(Mat4::IDENTITY * 2.0 / 2.0, Mat4::IDENTITY);
}

#[test]
fn div_assign() {
    let mut m: Mat4 = Mat4::IDENTITY;
    m /= 2.0;
    assert_eq!(m, Mat4::IDENTITY / 2.0);
}

#[test]
fn partial_eq() {
    assert!(Mat4::IDENTITY == Mat4::IDENTITY);
}

#[test]
fn not_partial_eq() {
    assert!(Mat4::ZERO != Mat4::IDENTITY);
}

#[test]
fn neg() {
    assert_eq!(-Mat4::IDENTITY, Mat4::from_diagonal(Vec4::NEG_ONE));
}

#[test]
fn as_ref_array() {
    let r: &[f32; 16] = Mat4::ZERO.as_ref();
    assert_eq!(r, &[0.0; 16]);
}

#[test]
fn as_mut_array() {
    let mut m: Mat4 = Mat4::IDENTITY;
    let r: &mut [f32; 16] = m.as_mut();

    r[0] = 10.0;

    assert_eq!(
        m,
        Mat4::from_cols(Vec4::new(10.0, 0.0, 0.0, 0.0), Vec4::Y, Vec4::Z, Vec4::W)
    );
}

#[test]
fn display() {
    assert_eq!(
        format!("{}", Mat4::IDENTITY),
        "[(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)]"
    );
}

#[test]
fn display_with_precision() {
    assert_eq!(
        format!("{:.2}", Mat4::IDENTITY),
        "[(1.00, 0.00, 0.00, 0.00), (0.00, 1.00, 0.00, 0.00), (0.00, 0.00, 1.00, 0.00), (0.00, 0.00, 0.00, 1.00)]"
    );
}

#[test]
fn default() {
    assert_eq!(Mat4::default(), Mat4::IDENTITY);
}

#[test]
fn from_array_trait() {
    assert_eq!(
        Mat4::from([
            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
        ]),
        Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        )
    );
}

#[test]
fn array_from_mat4() {
    assert_eq!(
        <[f32; 16]>::from(Mat4::from_cols(
            Vec4::new(1.0, 2.0, 3.0, 4.0),
            Vec4::new(5.0, 6.0, 7.0, 8.0),
            Vec4::new(9.0, 10.0, 11.0, 12.0),
            Vec4::new(13.0, 14.0, 15.0, 16.0)
        )),
        [
            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0
        ]
    );
}

#[test]
fn from_affine3() {
    assert_eq!(Mat4::from(Affine3::IDENTITY), Mat4::IDENTITY);
}
