use tuphium::error::TuphiumError;
use tuphium::frame::csv::{read_csv, CsvData, CsvValue};
use tuphium::frame::dataframe::{Column, DataFrame};

fn mixed_df() -> DataFrame {
    DataFrame::from_csv_data(CsvData {
        header: vec!["age".into(), "score".into(), "active".into(), "name".into()],
        rows: vec![
            vec![
                CsvValue::Integer(30),
                CsvValue::Float(9.5),
                CsvValue::Boolean(true),
                CsvValue::Text("Alice".into()),
            ],
            vec![
                CsvValue::Integer(25),
                CsvValue::Float(8.0),
                CsvValue::Boolean(false),
                CsvValue::Text("Bob".into()),
            ],
            vec![
                CsvValue::Integer(40),
                CsvValue::Float(7.0),
                CsvValue::Boolean(true),
                CsvValue::Text("Cara".into()),
            ],
        ],
    })
    .unwrap()
}

#[test]
fn from_csv_data_shape() {
    let data = read_csv("data/breast_cancer.csv").unwrap();
    let n_header = data.header.len();
    let n_rows = data.rows.len();
    let df = DataFrame::from_csv_data(data).unwrap();
    assert_eq!(df.shape(), (n_rows, n_header));
}

#[test]
fn column_access_by_name() {
    let df = mixed_df();
    match df.column("age") {
        Some(Column::Integer(v)) => assert_eq!(v, &vec![30, 25, 40]),
        other => panic!("expected Integer column, got {:?}", other),
    }
    assert!(df.column("nonexistent").is_none());
}

#[test]
fn select_and_drop_work() {
    let df = mixed_df();
    let sub = df.select(&["name", "age"]).unwrap();
    assert_eq!(sub.column_names(), &["name", "age"]);
    assert_eq!(sub.shape(), (3, 2));

    let dropped = df.drop("active").unwrap();
    assert_eq!(dropped.column_names(), &["age", "score", "name"]);
    assert!(dropped.column("active").is_none());
}

#[test]
fn select_returns_column_not_found() {
    let df = mixed_df();
    assert_eq!(
        df.select(&["missing"]).unwrap_err(),
        TuphiumError::ColumnNotFound {
            operation: "DataFrame::select",
            column: "missing".to_string(),
        }
    );
}

#[test]
fn filter_mask_keeps_matching_rows() {
    let df = mixed_df();
    let mask: Vec<bool> = df
        .as_f64("age")
        .unwrap()
        .iter()
        .map(|&v| v > 28.0)
        .collect();
    let filtered = df.filter_mask(&mask).unwrap();
    assert_eq!(filtered.shape(), (2, 4));
    match filtered.column("name") {
        Some(Column::Text(v)) => assert_eq!(v, &vec!["Alice".to_string(), "Cara".to_string()]),
        other => panic!("expected Text column, got {:?}", other),
    }
}

#[test]
fn filter_mask_returns_length_mismatch() {
    let df = mixed_df();
    assert_eq!(
        df.filter_mask(&[true]).unwrap_err(),
        TuphiumError::LengthMismatch {
            operation: "DataFrame::filter_mask",
            expected: 3,
            actual: 1,
        }
    );
}

#[test]
fn select_rows_reorders_and_repeats() {
    let df = mixed_df();
    let picked = df.select_rows(&[2, 1, 0, 0]).unwrap();
    assert_eq!(picked.shape(), (4, 4));
    match picked.column("age") {
        Some(Column::Integer(v)) => assert_eq!(v, &vec![40, 25, 30, 30]),
        other => panic!("expected Integer column, got {:?}", other),
    }
}

#[test]
fn select_rows_returns_row_index_out_of_bounds() {
    let df = mixed_df();
    assert_eq!(
        df.select_rows(&[0, 3]).unwrap_err(),
        TuphiumError::RowIndexOutOfBounds {
            operation: "DataFrame::select_rows",
            row: 3,
            rows: 3,
        }
    );
}

#[test]
fn stats_mean_min_max() {
    let df = mixed_df();
    assert_eq!(df.sum("age"), Some(95.0));
    assert!((df.mean("age").unwrap() - 31.666666).abs() < 1e-4);
    assert_eq!(df.min("age"), Some(25.0));
    assert_eq!(df.max("age"), Some(40.0));
    assert!((df.std("age").unwrap() - 7.637626).abs() < 1e-4);
    assert_eq!(df.mean("name"), None);
}

#[test]
fn as_f64_returns_column_type_mismatch() {
    let df = mixed_df();
    assert_eq!(
        df.as_f64("name").unwrap_err(),
        TuphiumError::ColumnTypeMismatch {
            operation: "DataFrame::as_f64",
            column: "name".to_string(),
            expected: "numeric".to_string(),
            actual: "Text".to_string(),
        }
    );
}

#[test]
fn to_matrix_shape() {
    let df = mixed_df();
    let m = df.to_matrix().unwrap();
    assert_eq!(m.rows, 3);
    assert_eq!(m.cols, 2);
    assert_eq!(m.get(0, 0), Ok(30.0));
    assert_eq!(m.get(0, 1), Ok(9.5));
}

#[test]
fn to_matrix_returns_no_numeric_columns() {
    let df = DataFrame::from_columns(
        vec!["name".to_string()],
        vec![Column::Text(vec!["Alice".to_string()])],
    )
    .unwrap();
    assert_eq!(
        df.to_matrix().unwrap_err(),
        TuphiumError::NoNumericColumns {
            operation: "DataFrame::to_matrix",
        }
    );
}

#[test]
fn from_csv_data_returns_malformed_csv() {
    let data = CsvData {
        header: vec!["a".into(), "b".into()],
        rows: vec![vec![CsvValue::Integer(1)]],
    };
    assert_eq!(
        DataFrame::from_csv_data(data).unwrap_err(),
        TuphiumError::MalformedCsv {
            operation: "DataFrame::from_csv_data",
            row: 0,
            expected_fields: 2,
            actual_fields: 1,
        }
    );
}
