use tuphium::error::TuphiumError;
use tuphium::linalg::Matrix;

#[test]
fn zeros_has_correct_shape() {
    let m = Matrix::zeros(3, 4).unwrap();
    assert_eq!(m.rows, 3);
    assert_eq!(m.cols, 4);
    assert_eq!(m.data.len(), 12);
}

#[test]
fn zeros_all_elements_are_zero() {
    let m = Matrix::zeros(2, 3).unwrap();
    assert!(m.data.iter().all(|&v| v == 0.0));
}

#[test]
fn from_data_stores_row_major() {
    let m = Matrix::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
    assert_eq!(m.get(0, 0), Ok(1.0));
    assert_eq!(m.get(0, 1), Ok(2.0));
    assert_eq!(m.get(1, 0), Ok(3.0));
    assert_eq!(m.get(1, 1), Ok(4.0));
}

#[test]
fn from_data_returns_length_mismatch() {
    assert_eq!(
        Matrix::from_data(2, 2, vec![1.0, 2.0]).unwrap_err(),
        TuphiumError::LengthMismatch {
            operation: "Matrix::from_data",
            expected: 4,
            actual: 2,
        }
    );
}

#[test]
fn zeros_returns_dimension_overflow() {
    assert_eq!(
        Matrix::zeros(usize::MAX, 2).unwrap_err(),
        TuphiumError::DimensionOverflow {
            operation: "Matrix::zeros",
            rows: usize::MAX,
            cols: 2,
        }
    );
}

#[test]
fn set_then_get_round_trips() {
    let mut m = Matrix::zeros(3, 3).unwrap();
    assert_eq!(m.set(1, 2, 7.5), Ok(()));
    assert_eq!(m.get(1, 2), Ok(7.5));
    assert_eq!(m.get(0, 0), Ok(0.0));
    assert_eq!(m.get(2, 2), Ok(0.0));
}

#[test]
fn get_returns_index_out_of_bounds() {
    let matrix = Matrix::zeros(2, 2).unwrap();
    assert_eq!(
        matrix.get(2, 0).unwrap_err(),
        TuphiumError::IndexOutOfBounds {
            operation: "Matrix::get",
            row: 2,
            col: 0,
            rows: 2,
            cols: 2,
        }
    );
}

#[test]
fn fill_sets_every_element() {
    let mut m = Matrix::zeros(2, 3).unwrap();
    m.fill(5.0);
    assert!(m.data.iter().all(|&v| v == 5.0));
}

#[test]
fn matvec_identity_returns_input() {
    let m = Matrix::from_data(3, 3, vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap();
    assert_eq!(m.matvec(&[2.0, 3.0, 4.0]), Ok(vec![2.0, 3.0, 4.0]));
}

#[test]
fn matvec_known_result() {
    let m = Matrix::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
    assert_eq!(m.matvec(&[5.0, 6.0]), Ok(vec![17.0, 39.0]));
}

#[test]
fn matvec_returns_shape_mismatch() {
    let m = Matrix::zeros(2, 3).unwrap();
    assert_eq!(
        m.matvec(&[1.0, 2.0]).unwrap_err(),
        TuphiumError::ShapeMismatch {
            operation: "Matrix::matvec",
            expected: vec![3],
            actual: vec![2],
        }
    );
}

#[test]
fn t_matvec_known_result() {
    let m = Matrix::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
    assert_eq!(m.t_matvec(&[1.0, 1.0]), Ok(vec![4.0, 6.0]));
}

#[test]
fn matvec_t_matvec_are_transposes() {
    let m = Matrix::from_data(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
    let v = vec![1.0, 0.0, -1.0];
    let u = vec![1.0, 1.0];

    let mv = m.matvec(&v).unwrap();
    let mtu = m.t_matvec(&u).unwrap();

    let dot_mv_u: f32 = mv.iter().zip(&u).map(|(a, b)| a * b).sum();
    let dot_v_mtu: f32 = v.iter().zip(&mtu).map(|(a, b)| a * b).sum();

    assert!((dot_mv_u - dot_v_mtu).abs() < 1e-5);
}

#[test]
fn add_outer_on_zeros_equals_outer_product() {
    let mut m = Matrix::zeros(2, 2).unwrap();
    assert_eq!(m.add_outer(&[1.0, 2.0], &[3.0, 4.0]), Ok(()));
    assert_eq!(m.get(0, 0), Ok(3.0));
    assert_eq!(m.get(0, 1), Ok(4.0));
    assert_eq!(m.get(1, 0), Ok(6.0));
    assert_eq!(m.get(1, 1), Ok(8.0));
}

#[test]
fn add_outer_returns_shape_mismatch() {
    let mut m = Matrix::zeros(2, 2).unwrap();
    assert_eq!(
        m.add_outer(&[1.0, 2.0, 3.0], &[1.0, 2.0]).unwrap_err(),
        TuphiumError::ShapeMismatch {
            operation: "Matrix::add_outer",
            expected: vec![2, 2],
            actual: vec![3, 2],
        }
    );
}
