//! Tests for `BruteForceIndex`.

#![allow(clippy::unwrap_used)]

use super::super::{DistanceMetric, QuantizationLevel};
use super::*;

/// Helper: create a 3-dimensional config for compact tests.
fn test_config() -> VectorIndexConfig {
    VectorIndexConfig {
        dimensions: 3,
        metric: DistanceMetric::L2,
        quantization: QuantizationLevel::F32,
        connectivity: 16,
        ef_construction: 200,
        ef_search: 200,
    }
}

// -----------------------------------------------------------------------
// Unit tests
// -----------------------------------------------------------------------

#[test]
fn brute_force_new_is_empty() {
    let index = BruteForceIndex::new(test_config());
    assert!(index.is_empty());
    assert_eq!(index.len(), 0);
    assert_eq!(index.len_live(), 0);
}

#[test]
fn brute_force_insert_and_len() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 2.0, 3.0]).unwrap();
    index.insert(2, &[4.0, 5.0, 6.0]).unwrap();
    assert_eq!(index.len(), 2);
    assert_eq!(index.len_live(), 2);
    assert!(!index.is_empty());
}

#[test]
fn brute_force_dimension_mismatch() {
    let index = BruteForceIndex::new(test_config());
    let result = index.insert(1, &[1.0, 2.0]); // 2D into 3D index
    assert!(result.is_err());
    match result.unwrap_err() {
        VectorError::DimensionMismatch { expected, got } => {
            assert_eq!(expected, 3);
            assert_eq!(got, 2);
        }
        other => panic!("expected DimensionMismatch, got {other:?}"),
    }
}

#[test]
fn brute_force_search_dimension_mismatch() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 2.0, 3.0]).unwrap();
    let result = index.search(&[1.0, 2.0], 1, 200); // 2D query on 3D index
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        VectorError::DimensionMismatch {
            expected: 3,
            got: 2
        }
    ));
}

#[test]
fn brute_force_self_search_distance_zero() {
    let index = BruteForceIndex::new(test_config());
    let vec = [1.0, 2.0, 3.0];
    index.insert(1, &vec).unwrap();

    let results = index.search(&vec, 1, 200).unwrap();
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].id, 1);
    assert!((results[0].distance - 0.0).abs() < f32::EPSILON);
}

#[test]
fn brute_force_search_empty_index() {
    let index = BruteForceIndex::new(test_config());
    let results = index.search(&[1.0, 2.0, 3.0], 5, 200).unwrap();
    assert!(results.is_empty());
}

#[test]
fn brute_force_search_k_larger_than_index() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    index.insert(2, &[0.0, 1.0, 0.0]).unwrap();

    let results = index.search(&[1.0, 0.0, 0.0], 10, 200).unwrap();
    assert_eq!(results.len(), 2); // only 2 vectors exist
}

#[test]
fn brute_force_orthogonal_vectors_distance() {
    let index = BruteForceIndex::new(test_config());
    // Two unit vectors along different axes: L2^2 = 2.0
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    index.insert(2, &[0.0, 1.0, 0.0]).unwrap();

    let results = index.search(&[1.0, 0.0, 0.0], 2, 200).unwrap();
    assert_eq!(results[0].id, 1); // self is closest
    assert!((results[0].distance - 0.0).abs() < f32::EPSILON);
    assert_eq!(results[1].id, 2);
    assert!((results[1].distance - 2.0).abs() < f32::EPSILON);
}

#[test]
fn brute_force_identical_vectors_distance() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[3.0, 4.0, 5.0]).unwrap();
    index.insert(2, &[3.0, 4.0, 5.0]).unwrap();

    let results = index.search(&[3.0, 4.0, 5.0], 2, 200).unwrap();
    assert_eq!(results.len(), 2);
    assert!((results[0].distance - 0.0).abs() < f32::EPSILON);
    assert!((results[1].distance - 0.0).abs() < f32::EPSILON);
}

#[test]
fn brute_force_delete_and_search() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    index.insert(2, &[0.0, 1.0, 0.0]).unwrap();
    index.insert(3, &[0.0, 0.0, 1.0]).unwrap();

    index.delete(2).unwrap();

    let results = index.search(&[0.0, 1.0, 0.0], 3, 200).unwrap();
    assert_eq!(results.len(), 2);
    // Deleted vector 2 must not appear
    assert!(results.iter().all(|r| r.id != 2));
}

#[test]
fn brute_force_delete_not_found() {
    let index = BruteForceIndex::new(test_config());
    let result = index.delete(999);
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        VectorError::NotFound { id: 999 }
    ));
}

#[test]
fn brute_force_insert_replaces_existing() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    // Replace vector 1 with a different embedding
    index.insert(1, &[0.0, 0.0, 1.0]).unwrap();

    assert_eq!(index.len(), 1); // still just one vector
    let results = index.search(&[0.0, 0.0, 1.0], 1, 200).unwrap();
    assert_eq!(results[0].id, 1);
    assert!((results[0].distance - 0.0).abs() < f32::EPSILON);
}

#[test]
fn brute_force_filtered_search_excludes_non_matching() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    index.insert(2, &[0.0, 1.0, 0.0]).unwrap();
    index.insert(3, &[0.0, 0.0, 1.0]).unwrap();

    // Only allow even IDs
    let results = index
        .filtered_search(&[0.0, 1.0, 0.0], 3, 200, &|id| id % 2 == 0)
        .unwrap();

    assert_eq!(results.len(), 1);
    assert_eq!(results[0].id, 2);
}

#[test]
fn brute_force_filtered_search_empty_result() {
    let index = BruteForceIndex::new(test_config());
    index.insert(1, &[1.0, 0.0, 0.0]).unwrap();
    index.insert(3, &[0.0, 0.0, 1.0]).unwrap();

    // Filter rejects all (only even IDs, but we only have odd)
    let results = index
        .filtered_search(&[1.0, 0.0, 0.0], 3, 200, &|id| id % 2 == 0)
        .unwrap();

    assert!(results.is_empty());
}

#[test]
fn brute_force_save_load_roundtrip() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("test_index.bfvi");

    let config = test_config();
    let index = BruteForceIndex::new(config.clone());
    index.insert(1, &[1.0, 2.0, 3.0]).unwrap();
    index.insert(2, &[4.0, 5.0, 6.0]).unwrap();
    index.insert(3, &[7.0, 8.0, 9.0]).unwrap();

    index.save(&path).unwrap();

    let loaded = BruteForceIndex::load(&path, &config).unwrap();
    assert_eq!(loaded.len(), 3);

    // Verify search produces the same results
    let query = [1.0, 2.0, 3.0];
    let original_results = index.search(&query, 3, 200).unwrap();
    let loaded_results = loaded.search(&query, 3, 200).unwrap();

    assert_eq!(original_results.len(), loaded_results.len());
    for (orig, load) in original_results.iter().zip(loaded_results.iter()) {
        assert_eq!(orig.id, load.id);
        assert!((orig.distance - load.distance).abs() < f32::EPSILON);
    }
}

#[test]
fn brute_force_reserve_is_noop() {
    let index = BruteForceIndex::new(test_config());
    // Must not error
    index.reserve(1_000_000).unwrap();
    assert!(index.is_empty()); // no side effects
}

#[test]
fn l2_distance_sq_correctness() {
    // [1, 2, 3] vs [4, 5, 6] => (3^2 + 3^2 + 3^2) = 27
    let dist = l2_distance_sq(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]);
    assert!((dist - 27.0).abs() < f32::EPSILON);

    // Identical vectors => 0
    let dist = l2_distance_sq(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]);
    assert!((dist - 0.0).abs() < f32::EPSILON);

    // Single dimension
    let dist = l2_distance_sq(&[3.0], &[7.0]);
    assert!((dist - 16.0).abs() < f32::EPSILON);
}

#[test]
fn brute_force_is_send_and_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<BruteForceIndex>();
}

#[test]
fn vector_index_config_defaults() {
    let config = VectorIndexConfig::default();
    assert_eq!(config.dimensions, 1536);
    assert_eq!(config.metric, DistanceMetric::L2);
    assert_eq!(config.quantization, QuantizationLevel::F16);
    assert_eq!(config.connectivity, 16);
    assert_eq!(config.ef_construction, 400);
    assert_eq!(config.ef_search, 200);
}

// -----------------------------------------------------------------------
// Property tests
// -----------------------------------------------------------------------

#[allow(clippy::cast_precision_loss)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    /// Generate a deterministic vector from an ID and dimension index.
    /// This avoids needing `rand` -- the values are fully determined by
    /// the ID and dimension.
    fn deterministic_vector(id: u64, dims: usize) -> Vec<f32> {
        (0..dims)
            .map(|i| ((id as usize * 3 + i * 7) % 100) as f32 / 100.0 - 0.5)
            .collect()
    }

    /// Normalize a vector to unit length. Returns the zero vector unchanged
    /// (property tests generate non-zero vectors via the arithmetic above).
    fn normalize(v: &mut [f32]) {
        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
        if norm > f32::EPSILON {
            for x in v.iter_mut() {
                *x /= norm;
            }
        }
    }

    proptest! {
        #[test]
        fn insert_search_roundtrip(count in 1_usize..50) {
            let dims = 8;
            let config = VectorIndexConfig {
                dimensions: dims,
                metric: DistanceMetric::L2,
                quantization: QuantizationLevel::F32,
                connectivity: 16,
                ef_construction: 200,
                ef_search: 200,
            };
            let index = BruteForceIndex::new(config);

            // Insert deterministic vectors
            for id in 0..count as u64 {
                let mut vec = deterministic_vector(id, dims);
                normalize(&mut vec);
                index.insert(id, &vec).unwrap();
            }

            // Each vector should be its own nearest neighbor (distance ~ 0)
            for id in 0..count as u64 {
                let mut query = deterministic_vector(id, dims);
                normalize(&mut query);

                let results = index.search(&query, 1, 200).unwrap();
                prop_assert!(!results.is_empty());

                // The vector should find itself. Access the stored vector
                // to verify the distance is indeed ~0.
                let stored: Vec<f32> = index.vectors.read().unwrap()[&id].clone();
                let dist = l2_distance_sq(&query, &stored);
                prop_assert!(dist < 1e-6, "self-distance should be ~0, got {}", dist);
            }
        }

        #[test]
        fn delete_excludes_from_results(count in 2_usize..50) {
            let dims = 8;
            let config = VectorIndexConfig {
                dimensions: dims,
                metric: DistanceMetric::L2,
                quantization: QuantizationLevel::F32,
                connectivity: 16,
                ef_construction: 200,
                ef_search: 200,
            };
            let index = BruteForceIndex::new(config);

            for id in 0..count as u64 {
                let vec = deterministic_vector(id, dims);
                index.insert(id, &vec).unwrap();
            }

            // Delete the first vector
            index.delete(0).unwrap();

            // Search for all remaining vectors -- ID 0 must never appear
            let query = deterministic_vector(0, dims);
            let results = index.search(&query, count, 200).unwrap();

            for result in &results {
                prop_assert_ne!(result.id, 0, "deleted vector 0 found in results");
            }
            prop_assert_eq!(results.len(), count - 1);
        }

        #[test]
        fn filtered_search_honors_predicate(count in 1_usize..50) {
            let dims = 8;
            let config = VectorIndexConfig {
                dimensions: dims,
                metric: DistanceMetric::L2,
                quantization: QuantizationLevel::F32,
                connectivity: 16,
                ef_construction: 200,
                ef_search: 200,
            };
            let index = BruteForceIndex::new(config);

            for id in 0..count as u64 {
                let vec = deterministic_vector(id, dims);
                index.insert(id, &vec).unwrap();
            }

            // Filter: only even IDs
            let query = deterministic_vector(0, dims);
            let results = index
                .filtered_search(&query, count, 200, &|id| id % 2 == 0)
                .unwrap();

            for result in &results {
                prop_assert!(
                    result.id % 2 == 0,
                    "odd ID {} found in even-only filtered search",
                    result.id
                );
            }
        }

        #[test]
        fn results_sorted_by_distance(count in 1_usize..50) {
            let dims = 8;
            let config = VectorIndexConfig {
                dimensions: dims,
                metric: DistanceMetric::L2,
                quantization: QuantizationLevel::F32,
                connectivity: 16,
                ef_construction: 200,
                ef_search: 200,
            };
            let index = BruteForceIndex::new(config);

            for id in 0..count as u64 {
                let vec = deterministic_vector(id, dims);
                index.insert(id, &vec).unwrap();
            }

            let query = deterministic_vector(0, dims);
            let results = index.search(&query, count, 200).unwrap();

            // Verify ascending distance ordering
            for pair in results.windows(2) {
                prop_assert!(
                    pair[0].distance <= pair[1].distance,
                    "results not sorted: {} > {}",
                    pair[0].distance,
                    pair[1].distance
                );
            }
        }
    }
}
