//! Mock vector index for unit testing.
//!
//! [`MockVectorIndex`] returns predetermined results and records call history,
//! enabling unit tests of higher-level components that depend on `VectorIndex`.

use std::path::Path;
use std::sync::RwLock;

use super::{VectorError, VectorId, VectorIndex, VectorIndexConfig, VectorSearchResult};

// ---------------------------------------------------------------------------
// VectorIndexCall
// ---------------------------------------------------------------------------

/// Record of a method call on a [`MockVectorIndex`].
#[derive(Debug, Clone)]
pub enum VectorIndexCall {
    /// `insert()` was called with this ID.
    Insert { id: VectorId },
    /// `delete()` was called with this ID.
    Delete { id: VectorId },
    /// `search()` was called with these parameters.
    Search { k: usize, ef_search: usize },
    /// `filtered_search()` was called with these parameters.
    FilteredSearch { k: usize, ef_search: usize },
    /// `reserve()` was called with this count.
    Reserve { additional: usize },
    /// `save()` was called.
    Save,
    /// `load()` was called.
    Load,
    /// `view()` was called.
    View,
}

// ---------------------------------------------------------------------------
// MockVectorIndex
// ---------------------------------------------------------------------------

/// A mock vector index that returns predetermined search results and records
/// all method calls for assertion in tests.
///
/// Each call to `search()` or `filtered_search()` pops the first element from
/// the predetermined results queue. If the queue is empty, an empty `Vec` is
/// returned.
pub struct MockVectorIndex {
    search_results: RwLock<Vec<Vec<VectorSearchResult>>>,
    call_log: RwLock<Vec<VectorIndexCall>>,
    config: VectorIndexConfig,
    inserted_count: RwLock<usize>,
}

impl MockVectorIndex {
    /// Create a new mock with predetermined search results.
    ///
    /// Each call to `search()` or `filtered_search()` drains the first element.
    #[must_use]
    pub const fn new(
        config: VectorIndexConfig,
        search_results: Vec<Vec<VectorSearchResult>>,
    ) -> Self {
        Self {
            search_results: RwLock::new(search_results),
            call_log: RwLock::new(Vec::new()),
            config,
            inserted_count: RwLock::new(0),
        }
    }

    /// Return the index configuration.
    #[must_use]
    pub const fn config(&self) -> &VectorIndexConfig {
        &self.config
    }

    /// Return a copy of all recorded calls.
    #[must_use]
    pub fn calls(&self) -> Vec<VectorIndexCall> {
        self.call_log
            .read()
            .map_or_else(|_| Vec::new(), |guard| guard.clone())
    }

    /// Clear the call log.
    pub fn clear_calls(&self) {
        if let Ok(mut guard) = self.call_log.write() {
            guard.clear();
        }
    }

    /// Record a call in the log.
    fn record(&self, call: VectorIndexCall) {
        if let Ok(mut guard) = self.call_log.write() {
            guard.push(call);
        }
    }

    /// Pop the next predetermined search result.
    fn next_result(&self) -> Vec<VectorSearchResult> {
        self.search_results
            .write()
            .ok()
            .and_then(|mut guard| {
                if guard.is_empty() {
                    None
                } else {
                    Some(guard.remove(0))
                }
            })
            .unwrap_or_default()
    }
}

impl VectorIndex for MockVectorIndex {
    fn insert(&self, id: VectorId, _embedding: &[f32]) -> Result<(), VectorError> {
        self.record(VectorIndexCall::Insert { id });
        if let Ok(mut count) = self.inserted_count.write() {
            *count += 1;
        }
        Ok(())
    }

    fn search(
        &self,
        _query: &[f32],
        k: usize,
        ef_search: usize,
    ) -> Result<Vec<VectorSearchResult>, VectorError> {
        self.record(VectorIndexCall::Search { k, ef_search });
        Ok(self.next_result())
    }

    fn filtered_search(
        &self,
        _query: &[f32],
        k: usize,
        ef_search: usize,
        _filter: &dyn Fn(VectorId) -> bool,
    ) -> Result<Vec<VectorSearchResult>, VectorError> {
        self.record(VectorIndexCall::FilteredSearch { k, ef_search });
        Ok(self.next_result())
    }

    fn delete(&self, id: VectorId) -> Result<(), VectorError> {
        self.record(VectorIndexCall::Delete { id });
        Ok(())
    }

    fn reserve(&self, additional: usize) -> Result<(), VectorError> {
        self.record(VectorIndexCall::Reserve { additional });
        Ok(())
    }

    fn save(&self, _path: &Path) -> Result<(), VectorError> {
        self.record(VectorIndexCall::Save);
        Ok(())
    }

    fn load(_path: &Path, config: &VectorIndexConfig) -> Result<Self, VectorError> {
        let instance = Self::new(config.clone(), Vec::new());
        instance.record(VectorIndexCall::Load);
        Ok(instance)
    }

    fn view(_path: &Path, config: &VectorIndexConfig) -> Result<Self, VectorError> {
        let instance = Self::new(config.clone(), Vec::new());
        instance.record(VectorIndexCall::View);
        Ok(instance)
    }

    fn len(&self) -> usize {
        self.inserted_count.read().map_or(0, |guard| *guard)
    }

    fn len_live(&self) -> usize {
        self.len()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    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,
        }
    }

    #[test]
    fn mock_vector_index_returns_predetermined() {
        let config = test_config();
        let batch_1 = vec![
            VectorSearchResult {
                id: 10,
                distance: 0.1,
            },
            VectorSearchResult {
                id: 20,
                distance: 0.5,
            },
        ];
        let batch_2 = vec![VectorSearchResult {
            id: 30,
            distance: 0.2,
        }];

        let mock = MockVectorIndex::new(config, vec![batch_1, batch_2]);

        // First search returns batch_1
        let r1 = mock.search(&[0.0, 0.0, 0.0], 5, 200).unwrap();
        assert_eq!(r1.len(), 2);
        assert_eq!(r1[0].id, 10);
        assert_eq!(r1[1].id, 20);

        // Second search returns batch_2
        let r2 = mock.search(&[0.0, 0.0, 0.0], 5, 200).unwrap();
        assert_eq!(r2.len(), 1);
        assert_eq!(r2[0].id, 30);

        // Third search: queue exhausted, returns empty
        let r3 = mock.search(&[0.0, 0.0, 0.0], 5, 200).unwrap();
        assert!(r3.is_empty());
    }

    #[test]
    fn mock_vector_index_records_calls() {
        let config = test_config();
        let mock = MockVectorIndex::new(config, Vec::new());

        mock.insert(1, &[1.0, 2.0, 3.0]).unwrap();
        mock.insert(2, &[4.0, 5.0, 6.0]).unwrap();
        let _ = mock.search(&[0.0, 0.0, 0.0], 5, 200);
        let _ = mock.filtered_search(&[0.0, 0.0, 0.0], 3, 100, &|_| true);
        mock.delete(1).unwrap();
        mock.reserve(100).unwrap();

        let calls = mock.calls();
        assert_eq!(calls.len(), 6);
        assert!(matches!(calls[0], VectorIndexCall::Insert { id: 1 }));
        assert!(matches!(calls[1], VectorIndexCall::Insert { id: 2 }));
        assert!(matches!(
            calls[2],
            VectorIndexCall::Search {
                k: 5,
                ef_search: 200
            }
        ));
        assert!(matches!(
            calls[3],
            VectorIndexCall::FilteredSearch {
                k: 3,
                ef_search: 100
            }
        ));
        assert!(matches!(calls[4], VectorIndexCall::Delete { id: 1 }));
        assert!(matches!(
            calls[5],
            VectorIndexCall::Reserve { additional: 100 }
        ));

        mock.clear_calls();
        assert!(mock.calls().is_empty());
    }

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