use std::f32::consts::FRAC_PI_2;

use roofy::{Mat2, Mat3, Vec2};

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

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

#[test]
fn const_identity() {
    assert_eq!(Mat2::IDENTITY, Mat2::from_cols(Vec2::X, Vec2::Y));
}

#[test]
fn new() {
    assert_eq!(
        Mat2::new(1.0, 2.0, 3.0, 4.0),
        Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),)
    );
}

#[test]
fn from_cols() {
    assert_eq!(
        Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),),
        Mat2 {
            col0: Vec2::new(1.0, 2.0),
            col1: Vec2::new(3.0, 4.0),
        }
    );
}

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

#[test]
fn from_array() {
    assert_eq!(
        Mat2::from_array([1.0, 2.0, 3.0, 4.0]),
        Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),)
    );
}

#[test]
fn to_array() {
    assert_eq!(
        Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),).to_array(),
        [1.0, 2.0, 3.0, 4.0]
    );
}

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

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

#[test]
fn write_to_slice() {
    let mut slice: [f32; 4] = [0.0; 4];
    Mat2::IDENTITY.write_to_slice(&mut slice);
    assert_eq!(slice, [1.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; 3] = [0.0; 3];
    Mat2::IDENTITY.write_to_slice(&mut slice);
}

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

#[test]
fn from_angle() {
    let (sin, cos): (f32, f32) = FRAC_PI_2.sin_cos();

    assert_eq!(Mat2::from_angle(FRAC_PI_2), Mat2::new(cos, sin, -sin, cos));
}

#[test]
fn from_angle_degrees() {
    assert_eq!(Mat2::from_angle_degrees(90.0), Mat2::from_angle(FRAC_PI_2));
}

#[test]
fn from_scale_angle_mathces_from_angle() {
    let angle: f32 = FRAC_PI_2;
    let a: Mat2 = Mat2::from_scale_angle(Vec2::ONE, angle);
    let b: Mat2 = Mat2::from_angle(angle);

    assert_eq!(a, b);
}

#[test]
fn from_scale_angle_matches_from_diagonal() {
    let scale: Vec2 = Vec2::new(2.0, 3.0);
    let a: Mat2 = Mat2::from_scale_angle(scale, 0.0);
    let b: Mat2 = Mat2::from_diagonal(scale);

    assert_eq!(a, b);
}

#[test]
fn from_scale_angle_matches_scale_then_rotate() {
    let scale = Vec2::new(2.0, 3.0);
    let angle = FRAC_PI_2;

    let combined = Mat2::from_scale_angle(scale, angle);
    let separate = Mat2::from_angle(angle) * Mat2::from_diagonal(scale);

    assert!(combined.approx_eq(separate));
}

#[test]
fn cols() {
    assert_eq!(Mat2::IDENTITY.cols(), [Vec2::X, Vec2::Y]);
}

#[test]
fn col_matches() {
    assert_eq!(Mat2::IDENTITY.col(0), Mat2::IDENTITY.col0);
    assert_eq!(Mat2::IDENTITY.col(1), Mat2::IDENTITY.col1);
}

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

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

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

#[test]
fn rows() {
    assert_eq!(Mat2::IDENTITY.rows(), [Vec2::X, Vec2::Y]);
}

#[test]
fn row_matches() {
    assert_eq!(Mat2::IDENTITY.row(0), Mat2::IDENTITY.row0());
    assert_eq!(Mat2::IDENTITY.row(1), Mat2::IDENTITY.row1());
}

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

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

#[test]
fn transpose_swaps_rows_and_columns() {
    let m: Mat2 = Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0));

    assert_eq!(
        m.transpose(),
        Mat2::from_cols(Vec2::new(1.0, 3.0), Vec2::new(2.0, 4.0),)
    )
}

#[test]
fn transpose_twice_stays_same() {
    let m: Mat2 = Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0));

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

#[test]
fn diagonal() {
    assert_eq!(Mat2::IDENTITY.diagonal(), Vec2::ONE);
}

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

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

#[test]
fn determinant_duplicate_rows_is_zero() {
    assert_eq!(
        Mat2::from_cols(Vec2::new(1.0, 1.0), Vec2::new(2.0, 2.0),).determinant(),
        0.0
    );
}

#[test]
fn determinant_scale_is_product_of_diagonal() {
    assert_eq!(Mat2::from_diagonal(Vec2::new(2.0, 3.0)).determinant(), 6.0);
}

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

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

#[test]
fn inverse_times_original_is_identity() {
    let cases: [Mat2; 3] = [
        Mat2::IDENTITY,
        Mat2::from_diagonal(Vec2::new(2.0, 0.5)),
        Mat2::from_angle(FRAC_PI_2),
    ];

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

#[test]
fn inverse_scale() {
    assert_eq!(
        Mat2::from_diagonal(Vec2::new(2.0, 3.0)).inverse(),
        Mat2::from_diagonal(Vec2::new(0.5, 1.0 / 3.0))
    );
}

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

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

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

#[test]
fn transform_identity_stays_same() {
    let v: Vec2 = Vec2::new(1.0, 2.0);

    assert_eq!(Mat2::IDENTITY.transform(v), v);
}

#[test]
fn transform_zero_returns_zero() {
    let v: Vec2 = Vec2::new(1.0, 2.0);

    assert_eq!(Mat2::ZERO.transform(v), Vec2::ZERO);
}

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

#[test]
fn recip() {
    assert_eq!(
        Mat2::IDENTITY.recip(),
        Mat2::from_cols(Vec2::X.recip(), Vec2::Y.recip())
    );
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#[test]
fn mul_identity_is_noop() {
    let m: Mat2 = Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0));

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

#[test]
fn mul_zero_returns_zero() {
    let m: Mat2 = Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0));

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

#[test]
fn mul_scalar_diagonal() {
    let a: Mat2 = Mat2::from_diagonal(Vec2::splat(2.0));
    let b: Mat2 = Mat2::from_diagonal(Vec2::splat(3.0));
    let expected: Mat2 = Mat2::from_diagonal(Vec2::splat(6.0));

    assert_eq!(a * b, expected);
}

#[test]
fn mul_vec2_identity_stays_same() {
    let v: Vec2 = Vec2::new(1.0, 2.0);

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

#[test]
fn mul_vec2_zero_returns_zero() {
    let v: Vec2 = Vec2::new(1.0, 2.0);

    assert_eq!(Mat2::ZERO * v, Vec2::ZERO);
}

#[test]
fn mul_f32() {
    assert_eq!(
        Mat2::IDENTITY * 2.0,
        Mat2::from_cols(Vec2::X * 2.0, Vec2::Y * 2.0)
    );
}

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

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

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

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

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

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

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

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

    r[0] = 10.0;

    assert_eq!(m, Mat2::from_cols(Vec2::new(10.0, 0.0), Vec2::Y));
}

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

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

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

#[test]
fn from_array_trait() {
    assert_eq!(
        Mat2::from([1.0, 2.0, 3.0, 4.0]),
        Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),)
    );
}

#[test]
fn array_from_mat4() {
    assert_eq!(
        <[f32; 4]>::from(Mat2::from_cols(Vec2::new(1.0, 2.0), Vec2::new(3.0, 4.0),)),
        [1.0, 2.0, 3.0, 4.0]
    );
}

#[test]
fn from_mat3_trait() {
    assert_eq!(Mat2::from(Mat3::IDENTITY), Mat2::IDENTITY);
}
