//! B-tree backed range index for ordered numeric fields.
//!
//! Maps attribute values to `RoaringBitmap` sets of entity IDs.
//! The B-tree ordering enables efficient range queries.

use std::{collections::BTreeMap, ops::Bound, sync::RwLock};

use roaring::RoaringBitmap;

use super::{ENCODED_KEY_PREFIX_LEN, IndexError};
use crate::{
    schema::EntityId,
    storage::{
        indexes::bitmap::INDEX_ROOT_ID,
        keys::{Tag, encode_key},
    },
};

/// Serialized key-value pairs for index persistence.
type KvPairs = Vec<(Vec<u8>, Vec<u8>)>;

/// A B-tree backed range index for a single ordered numeric field.
///
/// Maps attribute values to `RoaringBitmap` sets of entity IDs.
/// The B-tree ordering enables range queries: "all entities with duration >= 300"
/// unions bitmaps for keys 300, 301, ...
///
/// # Concurrency
///
/// Same model as `BitmapIndex`: `RwLock` for read/write separation.
pub struct RangeIndex<V: Ord + Clone> {
    field_name: String,
    tree: RwLock<BTreeMap<V, RoaringBitmap>>,
    /// Cached distinct-entity union cardinality, invalidated on every mutation.
    ///
    /// `total_count()` unions every value bitmap — `O(distinct_values *
    /// avg_bitmap_size)`. The query planner calls it (directly and via
    /// `selectivity`) once per range predicate while ordering AND/OR groups, so
    /// a multi-predicate query re-walked all value bitmaps repeatedly between
    /// writes. The cache makes those repeated reads O(1): writes set it to
    /// `None`, the next read recomputes once and memoizes. It is a read-side
    /// memo of state already in `tree`, never an independent source of truth, so
    /// it cannot drift — a missed invalidation would only force a recompute.
    total_count_cache: RwLock<Option<u64>>,
}

impl<V: Ord + Clone> RangeIndex<V> {
    /// Create a new, empty range index for the given field.
    #[must_use]
    pub fn new(field_name: impl Into<String>) -> Self {
        Self {
            field_name: field_name.into(),
            tree: RwLock::new(BTreeMap::new()),
            total_count_cache: RwLock::new(Some(0)),
        }
    }

    /// Invalidate the memoized total-count union after a mutation.
    fn invalidate_total_count(&self) {
        *self
            .total_count_cache
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
    }

    /// The field name this index covers.
    #[must_use]
    pub fn field_name(&self) -> &str {
        &self.field_name
    }

    /// Add an entity with the given attribute value.
    pub fn insert(&self, entity_id: u32, value: V) {
        let mut tree = self
            .tree
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let bitmap = tree.entry(value).or_default();
        bitmap.insert(entity_id);
        drop(tree);
        self.invalidate_total_count();
    }

    /// Remove an entity from the bitmap at the given value.
    ///
    /// Returns `true` if the entity was present and removed.
    pub fn delete(&self, entity_id: u32, value: &V) -> bool {
        let mut tree = self
            .tree
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        // Capture both the removal result and whether the bitmap is now empty in
        // the single `get_mut` borrow, so the cleanup branch below needs no
        // second tree lookup.
        let Some(bitmap) = tree.get_mut(value) else {
            return false;
        };
        let removed = bitmap.remove(entity_id);
        let now_empty = bitmap.is_empty();
        // Clean up empty bitmaps.
        if now_empty {
            tree.remove(value);
        }
        drop(tree);
        if removed {
            self.invalidate_total_count();
        }
        removed
    }

    /// Remove an entity from EVERY value bitmap in the tree.
    ///
    /// Returns the number of value bitmaps the entity was removed from.
    ///
    /// Mirrors [`BitmapIndex::delete_entity`](crate::storage::indexes::BitmapIndex::delete_entity):
    /// re-indexing an item rewrites its metadata, which may move it to a new
    /// numeric value (e.g. `duration` 300 -> 600). Without scrubbing the old
    /// value first, the entity would remain indexed under its prior value and a
    /// range query would return a stale hit. Callers on the re-index path must
    /// call this before re-inserting under the new value.
    pub fn delete_entity(&self, entity_id: u32) -> usize {
        let mut tree = self
            .tree
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let mut count = 0;
        let mut empty_keys = Vec::new();
        for (key, bitmap) in tree.iter_mut() {
            if bitmap.remove(entity_id) {
                count += 1;
                if bitmap.is_empty() {
                    empty_keys.push(key.clone());
                }
            }
        }
        for key in empty_keys {
            tree.remove(&key);
        }
        drop(tree);
        if count > 0 {
            self.invalidate_total_count();
        }
        count
    }

    /// Range query: union of all bitmaps with keys in [lo, hi].
    ///
    /// Inverted bounds (lo strictly greater than hi, or equal-but-excluded
    /// endpoints describing an empty interval) yield an empty bitmap rather
    /// than panicking. `BTreeMap::range` panics on such inputs, and `range`
    /// is reachable through the public filter API, so we clamp to the empty
    /// result here.
    #[must_use]
    pub fn range(&self, lo: Bound<&V>, hi: Bound<&V>) -> RoaringBitmap {
        if Self::is_empty_interval(lo, hi) {
            return RoaringBitmap::new();
        }
        let tree = self
            .tree
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let mut result = RoaringBitmap::new();
        for (_key, bitmap) in tree.range((lo, hi)) {
            result |= bitmap;
        }
        drop(tree);
        result
    }

    /// Returns `true` if `(lo, hi)` describes an interval `BTreeMap::range`
    /// would reject (i.e., `lo` is strictly above `hi`, or the bounds coincide
    /// while at least one endpoint is excluded).
    ///
    /// `BTreeMap::range` panics if `start > end` or if `start == end` and both
    /// bounds are `Excluded`. We treat every such case as the empty interval.
    fn is_empty_interval(lo: Bound<&V>, hi: Bound<&V>) -> bool {
        let (low, low_inclusive) = match lo {
            Bound::Unbounded => return false,
            Bound::Included(v) => (v, true),
            Bound::Excluded(v) => (v, false),
        };
        let (high, high_inclusive) = match hi {
            Bound::Unbounded => return false,
            Bound::Included(v) => (v, true),
            Bound::Excluded(v) => (v, false),
        };
        match low.cmp(high) {
            std::cmp::Ordering::Greater => true,
            // Equal endpoints are empty unless both are inclusive: [x, x] is a
            // single point, but (x, x], [x, x), and (x, x) are all empty.
            std::cmp::Ordering::Equal => !(low_inclusive && high_inclusive),
            std::cmp::Ordering::Less => false,
        }
    }

    /// Greater-than query: entities with value > threshold.
    #[must_use]
    pub fn gt(&self, threshold: &V) -> RoaringBitmap {
        self.range(Bound::Excluded(threshold), Bound::Unbounded)
    }

    /// Greater-than-or-equal query.
    #[must_use]
    pub fn gte(&self, threshold: &V) -> RoaringBitmap {
        self.range(Bound::Included(threshold), Bound::Unbounded)
    }

    /// Less-than query: entities with value < threshold.
    #[must_use]
    pub fn lt(&self, threshold: &V) -> RoaringBitmap {
        self.range(Bound::Unbounded, Bound::Excluded(threshold))
    }

    /// Less-than-or-equal query.
    #[must_use]
    pub fn lte(&self, threshold: &V) -> RoaringBitmap {
        self.range(Bound::Unbounded, Bound::Included(threshold))
    }

    /// Estimate fraction of entities in range. Returns value in [0.0, 1.0].
    ///
    /// Returns 0.0 if `total` is 0.
    #[must_use]
    pub fn selectivity(&self, lo: Bound<&V>, hi: Bound<&V>, total: u64) -> f64 {
        if total == 0 {
            return 0.0;
        }
        let range_bitmap = self.range(lo, hi);
        #[allow(clippy::cast_precision_loss)]
        let result = range_bitmap.len() as f64 / total as f64;
        result.clamp(0.0, 1.0)
    }

    /// Total distinct entity IDs indexed.
    ///
    /// Memoized: the union over every value bitmap is computed at most once per
    /// mutation. Repeated reads between writes (the planner's per-predicate
    /// selectivity ordering) are O(1).
    #[must_use]
    pub fn total_count(&self) -> u64 {
        // Fast path: return the memoized cardinality if still valid. Read the
        // cache into a local and drop the guard before branching so the cache
        // lock is never held across the slow-path tree read.
        let cached = *self
            .total_count_cache
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(count) = cached {
            return count;
        }
        // Slow path: recompute the union once and memoize it. Recompute under the
        // tree read lock so the snapshot we count is internally consistent.
        let tree = self
            .tree
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let mut union = RoaringBitmap::new();
        for bitmap in tree.values() {
            union |= bitmap;
        }
        drop(tree);
        let count = union.len();
        *self
            .total_count_cache
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(count);
        count
    }

    /// Number of distinct attribute values in the tree.
    #[must_use]
    pub fn distinct_values(&self) -> usize {
        let tree = self
            .tree
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        tree.len()
    }

    /// Whether the index is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        let tree = self
            .tree
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        tree.is_empty()
    }
}

// `RwLock<BTreeMap<V, RoaringBitmap>>` is `Send + Sync` when `V: Send + Sync`.
// The compiler derives these automatically; no manual unsafe impl is needed.

// === Persistence ===

/// Serialized-key tag distinguishing range-index keys from other index keys
/// sharing the [`Tag::Idx`] namespace (e.g. the bitmap index's `"BMP:"`).
///
/// Both `serialize_to_kv_pairs` and `load_from_kv_pairs` derive their key
/// prefix from this single constant so the write and read sides cannot drift.
/// The full per-field prefix is `"{RANGE_KEY_PREFIX}{field_name}:"`.
const RANGE_KEY_PREFIX: &str = "RNG:";

/// Fixed-width big-endian codec for range-index key values.
///
/// Implemented for the concrete numeric value types a `RangeIndex` persists
/// (`u32`, `u64`). Centralizes the only difference between the otherwise
/// identical serialize/load paths: how many big-endian bytes a value occupies
/// in the key suffix.
pub trait RangeKeyCodec: Ord + Clone {
    /// Number of big-endian bytes this value occupies in a key suffix.
    const WIDTH: usize;
    /// Human-readable type name for truncation error messages.
    const TYPE_NAME: &'static str;

    /// Encode the value as big-endian bytes appended to a key suffix.
    fn to_be_key_bytes(&self) -> Vec<u8>;
    /// Decode a value from the leading `WIDTH` bytes of `raw`.
    fn from_be_key_bytes(raw: &[u8]) -> Self;
}

impl RangeKeyCodec for u32 {
    const WIDTH: usize = 4;
    const TYPE_NAME: &'static str = "u32";

    fn to_be_key_bytes(&self) -> Vec<u8> {
        self.to_be_bytes().to_vec()
    }

    fn from_be_key_bytes(raw: &[u8]) -> Self {
        // Caller guarantees `raw.len() >= WIDTH`; the fixed-size slice cannot fail.
        let mut buf = [0u8; 4];
        buf.copy_from_slice(&raw[..4]);
        Self::from_be_bytes(buf)
    }
}

impl RangeKeyCodec for u64 {
    const WIDTH: usize = 8;
    const TYPE_NAME: &'static str = "u64";

    fn to_be_key_bytes(&self) -> Vec<u8> {
        self.to_be_bytes().to_vec()
    }

    fn from_be_key_bytes(raw: &[u8]) -> Self {
        // Caller guarantees `raw.len() >= WIDTH`; the fixed-size slice cannot fail.
        let mut buf = [0u8; 8];
        buf.copy_from_slice(&raw[..8]);
        Self::from_be_bytes(buf)
    }
}

impl<V: RangeKeyCodec> RangeIndex<V> {
    /// Serialize all bitmaps to (key, value) pairs for storage.
    ///
    /// Key suffix: `b"RNG:{field_name}:{value_be_bytes}"`.
    ///
    /// # Errors
    ///
    /// Returns `IndexError::Serialization` if serialization fails.
    pub fn serialize_to_kv_pairs(&self) -> Result<KvPairs, IndexError> {
        let tree = self
            .tree
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let mut pairs = Vec::with_capacity(tree.len());
        for (value, bitmap) in tree.iter() {
            let mut suffix = format!("{RANGE_KEY_PREFIX}{}:", self.field_name).into_bytes();
            suffix.extend_from_slice(&value.to_be_key_bytes());
            let key = encode_key(EntityId::new(INDEX_ROOT_ID), Tag::Idx, &suffix);
            let mut bytes = Vec::new();
            bitmap
                .serialize_into(&mut bytes)
                .map_err(|e| IndexError::Serialization(e.to_string()))?;
            pairs.push((key, bytes));
        }
        drop(tree);
        Ok(pairs)
    }

    /// Deserialize from (key, value) pairs.
    ///
    /// # Errors
    ///
    /// Returns `IndexError::Serialization` if a matching key's value field is not
    /// exactly `V::WIDTH` bytes (truncated *or* carrying trailing bytes) or if a
    /// serialized bitmap is corrupt.
    pub fn load_from_kv_pairs(
        field_name: impl Into<String>,
        pairs: impl Iterator<Item = (Vec<u8>, Vec<u8>)>,
    ) -> Result<Self, IndexError> {
        let field_name = field_name.into();
        let prefix_bytes = format!("{RANGE_KEY_PREFIX}{field_name}:").into_bytes();
        let mut tree = BTreeMap::new();

        for (key, value_bytes) in pairs {
            // Skip the fixed encoded-key prefix (8 entity id + 1 NUL + 1 tag).
            if key.len() < ENCODED_KEY_PREFIX_LEN {
                continue;
            }
            let suffix_raw = &key[ENCODED_KEY_PREFIX_LEN..];
            if !suffix_raw.starts_with(&prefix_bytes) {
                continue;
            }
            let value_raw = &suffix_raw[prefix_bytes.len()..];
            // A well-formed range key carries EXACTLY `V::WIDTH` value bytes.
            // Reject both truncation (too few) and trailing garbage (too many):
            // `from_be_key_bytes` reads only `raw[..WIDTH]` and would silently
            // discard a remainder, so two adversarial keys sharing the first
            // WIDTH value bytes but differing in their tail would collapse onto
            // the same tree entry (last-write-wins), masking corruption. Require
            // an exact width so a malformed key surfaces as an error instead.
            if value_raw.len() != V::WIDTH {
                return Err(IndexError::Serialization(format!(
                    "malformed {} value in key: expected exactly {} bytes, got {}",
                    V::TYPE_NAME,
                    V::WIDTH,
                    value_raw.len()
                )));
            }
            let value = V::from_be_key_bytes(value_raw);
            let bitmap = RoaringBitmap::deserialize_from(value_bytes.as_slice())
                .map_err(|e| IndexError::Serialization(e.to_string()))?;
            if !bitmap.is_empty() {
                tree.insert(value, bitmap);
            }
        }

        Ok(Self {
            field_name,
            tree: RwLock::new(tree),
            // Loaded fresh; force a recompute on first read.
            total_count_cache: RwLock::new(None),
        })
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
    use proptest::prelude::*;

    use super::*;

    // === Unit tests ===

    #[test]
    fn new_index_is_empty() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        assert!(index.is_empty());
        assert_eq!(index.total_count(), 0);
        assert_eq!(index.distinct_values(), 0);
    }

    #[test]
    fn insert_and_range_query() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);
        index.insert(4, 1800);

        let result = index.range(Bound::Included(&300), Bound::Included(&600));
        assert_eq!(result.len(), 2);
        assert!(result.contains(2));
        assert!(result.contains(3));
        assert!(!result.contains(1));
        assert!(!result.contains(4));
    }

    #[test]
    fn gte_query() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        let result = index.gte(&300);
        assert_eq!(result.len(), 2);
        assert!(result.contains(2));
        assert!(result.contains(3));
    }

    #[test]
    fn gt_query() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        let result = index.gt(&300);
        assert_eq!(result.len(), 1);
        assert!(result.contains(3));
    }

    #[test]
    fn lt_and_lte_queries() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        let lt = index.lt(&300);
        assert_eq!(lt.len(), 1);
        assert!(lt.contains(1));

        let lte = index.lte(&300);
        assert_eq!(lte.len(), 2);
        assert!(lte.contains(1));
        assert!(lte.contains(2));
    }

    #[test]
    fn unbounded_range_returns_all() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        let all = index.range(Bound::Unbounded, Bound::Unbounded);
        assert_eq!(all.len(), 3);
    }

    #[test]
    fn empty_range_returns_empty_bitmap() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);

        let result = index.range(Bound::Included(&400), Bound::Included(&500));
        assert!(result.is_empty());
    }

    #[test]
    fn selectivity_full_range() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        let sel = index.selectivity(Bound::Unbounded, Bound::Unbounded, 3);
        assert!((sel - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn selectivity_partial_range() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);
        index.insert(4, 1800);

        let sel = index.selectivity(Bound::Included(&300), Bound::Included(&600), 4);
        assert!((sel - 0.5).abs() < f64::EPSILON);
    }

    #[test]
    fn selectivity_zero_total() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        let sel = index.selectivity(Bound::Unbounded, Bound::Unbounded, 0);
        assert!((sel - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn inverted_bounds_return_empty_no_panic() {
        // BTreeMap::range panics on lo > hi; range() must clamp to empty instead.
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 60);
        index.insert(2, 300);
        index.insert(3, 600);

        // lo strictly greater than hi.
        let inverted = index.range(Bound::Included(&600), Bound::Included(&300));
        assert!(inverted.is_empty(), "inverted [600,300] must be empty");

        // Equal endpoints with an excluded side describe an empty interval.
        let excluded_lo = index.range(Bound::Excluded(&300), Bound::Included(&300));
        assert!(excluded_lo.is_empty(), "(300, 300] must be empty");
        let excluded_hi = index.range(Bound::Included(&300), Bound::Excluded(&300));
        assert!(excluded_hi.is_empty(), "[300, 300) must be empty");
        let both_excluded = index.range(Bound::Excluded(&300), Bound::Excluded(&300));
        assert!(both_excluded.is_empty(), "(300, 300) must be empty");

        // Equal inclusive endpoints are a valid single-point range.
        let point = index.range(Bound::Included(&300), Bound::Included(&300));
        assert_eq!(point.len(), 1, "[300, 300] must contain the single point");
        assert!(point.contains(2));
    }

    #[test]
    fn inverted_bounds_u64_return_empty_no_panic() {
        let index: RangeIndex<u64> = RangeIndex::new("created_at");
        index.insert(1, 1_000);
        index.insert(2, 2_000);
        let inverted = index.range(Bound::Included(&2_000), Bound::Excluded(&1_000));
        assert!(inverted.is_empty());
    }

    #[test]
    fn load_rejects_trailing_bytes_after_fixed_width_value() {
        // A valid range key carries exactly V::WIDTH value bytes. A key with the
        // same WIDTH-byte prefix but extra trailing bytes must be rejected, not
        // silently truncated onto the same tree entry (which would mask
        // corruption / let two distinct keys collide last-write-wins).
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        let pairs = index.serialize_to_kv_pairs().unwrap();
        assert_eq!(pairs.len(), 1);
        let (mut key, value) = pairs.into_iter().next().unwrap();
        // Append a trailing byte after the 4-byte u32 value.
        key.push(0xFF);

        let result =
            RangeIndex::<u32>::load_from_kv_pairs("duration", std::iter::once((key, value)));
        let is_serialization_err =
            matches!(&result, Err(IndexError::Serialization(m)) if m.contains("expected exactly"));
        assert!(
            is_serialization_err,
            "trailing bytes after a fixed-width value must be a Serialization error (got Ok or wrong variant)"
        );
    }

    #[test]
    fn total_count_cache_tracks_inserts_and_deletes() {
        // The memoized total_count must agree with the freshly-recomputed union
        // across a full insert/delete cycle, proving cache invalidation is wired
        // on every mutation (insert, delete, delete_entity).
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        assert_eq!(index.total_count(), 0); // primed-empty cache
        index.insert(1, 60);
        index.insert(2, 120);
        index.insert(3, 120);
        assert_eq!(index.total_count(), 3); // recomputed after invalidation
        assert_eq!(index.total_count(), 3); // memoized fast path agrees

        // Re-inserting the same id at a new value does not change the distinct
        // count (it is still one entity, now under two values until scrubbed).
        index.insert(1, 600);
        assert_eq!(index.total_count(), 3);

        // Deleting scrubs the id; the count drops only when no value still holds it.
        index.delete_entity(1);
        assert_eq!(index.total_count(), 2);
        index.delete(2, &120);
        assert_eq!(index.total_count(), 1);
        index.delete(3, &120);
        assert_eq!(index.total_count(), 0);
    }

    #[test]
    fn delete_cleans_up_empty_entries() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        index.delete(1, &300);

        assert_eq!(index.total_count(), 0);
        assert_eq!(index.distinct_values(), 0);
    }

    #[test]
    fn delete_entity_scrubs_all_values() {
        // An entity that was indexed under multiple values (e.g. before/after a
        // value change) must be fully scrubbed by delete_entity, leaving only
        // other entities' bitmaps behind.
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        index.insert(1, 600);
        index.insert(2, 300);

        let removed = index.delete_entity(1);
        assert_eq!(removed, 2, "entity 1 was present under two values");

        // The value-600 bucket emptied and was pruned; value-300 still holds 2.
        assert_eq!(index.distinct_values(), 1);
        assert!(
            index
                .range(Bound::Included(&300), Bound::Included(&300))
                .contains(2)
        );
        assert!(!index.range(Bound::Unbounded, Bound::Unbounded).contains(1));
        assert_eq!(index.total_count(), 1);
    }

    #[test]
    fn delete_entity_absent_is_noop() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        assert_eq!(index.delete_entity(99), 0);
        assert_eq!(index.distinct_values(), 1);
        assert_eq!(index.total_count(), 1);
    }

    #[test]
    fn reindex_via_delete_entity_then_insert_leaves_no_stale_entry() {
        // Reproduces W22: re-indexing an item that moved from duration 300 to
        // 600 must not leave a stale range entry at the old value. The re-index
        // path is `delete_entity` (scrub old) followed by `insert` (new value).
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        assert!(index.gte(&300).contains(1));

        index.delete_entity(1);
        index.insert(1, 600);

        // Stale value 300 must no longer match; only the new value 600 does.
        assert!(
            !index
                .range(Bound::Included(&300), Bound::Included(&300))
                .contains(1)
        );
        assert!(
            index
                .range(Bound::Included(&600), Bound::Included(&600))
                .contains(1)
        );
        assert_eq!(index.distinct_values(), 1);
    }

    #[test]
    fn multiple_entities_same_value() {
        let index: RangeIndex<u32> = RangeIndex::new("duration");
        index.insert(1, 300);
        index.insert(2, 300);
        index.insert(3, 300);

        let result = index.gte(&300);
        assert_eq!(result.len(), 3);
        assert_eq!(index.distinct_values(), 1);
    }

    #[test]
    fn timestamp_index_u64() {
        let index: RangeIndex<u64> = RangeIndex::new("created_at");
        let now_ns: u64 = 1_708_000_000_000_000_000;
        let one_day_ago = now_ns - 86_400_000_000_000;
        let seven_days_ago = now_ns - 7 * 86_400_000_000_000;

        index.insert(1, now_ns);
        index.insert(2, one_day_ago);
        index.insert(3, seven_days_ago);
        index.insert(4, seven_days_ago - 1);

        let recent = index.gte(&seven_days_ago);
        assert_eq!(recent.len(), 3);
        assert!(!recent.contains(4));
    }

    // === Property tests ===

    /// Build a `Bound` from a kind discriminant: 0 = Unbounded, 1 = Included,
    /// 2 = Excluded. Hoisted out of the proptest body so the proptest macro does
    /// not nest a fn after statements.
    fn make_bound(kind: u8, x: &u32) -> Bound<&u32> {
        match kind {
            1 => Bound::Included(x),
            2 => Bound::Excluded(x),
            _ => Bound::Unbounded,
        }
    }

    proptest! {
        #[test]
        fn range_query_correctness(
            entries in prop::collection::vec(
                (0u32..10_000, 0u32..1000),
                1..200,
            ),
            lo in 0u32..500,
            hi in 500u32..1000,
        ) {
            let index: RangeIndex<u32> = RangeIndex::new("test_field");
            for &(id, value) in &entries {
                index.insert(id, value);
            }

            let result = index.range(Bound::Included(&lo), Bound::Included(&hi));

            for &(id, value) in &entries {
                if value >= lo && value <= hi {
                    prop_assert!(result.contains(id),
                        "entity {id} with value {value} should be in range [{lo}, {hi}]");
                }
            }
        }

        #[test]
        fn range_never_panics_on_arbitrary_bounds(
            entries in prop::collection::vec(
                (0u32..10_000, 0u32..1000),
                0..100,
            ),
            a in 0u32..1000,
            b in 0u32..1000,
        ) {
            // Cover inverted, equal, and ordered bounds across all Bound kinds.
            let index: RangeIndex<u32> = RangeIndex::new("test_field");
            for &(id, value) in &entries {
                index.insert(id, value);
            }
            for lo_kind in 0u8..=2 {
                for hi_kind in 0u8..=2 {
                    // Must return without panicking regardless of ordering.
                    let _ = index.range(make_bound(lo_kind, &a), make_bound(hi_kind, &b));
                }
            }
        }

        #[test]
        fn selectivity_in_unit_range(
            entries in prop::collection::vec(
                (0u32..10_000, 0u32..1000),
                1..200,
            ),
            lo in 0u32..500,
            hi in 500u32..1000,
        ) {
            let index: RangeIndex<u32> = RangeIndex::new("test_field");
            for &(id, value) in &entries {
                index.insert(id, value);
            }
            let total = index.total_count();
            let sel = index.selectivity(Bound::Included(&lo), Bound::Included(&hi), total);
            prop_assert!(sel >= 0.0, "selectivity was {sel}");
            prop_assert!(sel <= 1.0, "selectivity was {sel}");
        }

        #[test]
        fn insert_delete_roundtrip(
            entries in prop::collection::vec(
                (0u32..1_000, 0u32..100),
                1..100,
            ),
        ) {
            let index: RangeIndex<u32> = RangeIndex::new("test_field");
            for &(id, value) in &entries {
                index.insert(id, value);
            }

            for &(id, value) in &entries {
                index.delete(id, &value);
            }

            let result = index.range(Bound::Unbounded, Bound::Unbounded);
            prop_assert!(result.is_empty(), "expected empty after deleting all");
        }

        #[test]
        fn serialize_roundtrip_u32(
            entries in prop::collection::vec(
                (0u32..10_000, 0u32..1000),
                1..100,
            ),
        ) {
            let index: RangeIndex<u32> = RangeIndex::new("duration");
            for &(id, value) in &entries {
                index.insert(id, value);
            }

            let kv_pairs = index.serialize_to_kv_pairs().unwrap();
            let restored = RangeIndex::<u32>::load_from_kv_pairs(
                "duration",
                kv_pairs.into_iter(),
            ).unwrap();

            let orig = index.range(Bound::Unbounded, Bound::Unbounded);
            let rest = restored.range(Bound::Unbounded, Bound::Unbounded);
            prop_assert_eq!(orig, rest);
        }

        #[test]
        fn serialize_roundtrip_u64(
            entries in prop::collection::vec((any::<u32>(), any::<u64>()), 1..50),
        ) {
            let index: RangeIndex<u64> = RangeIndex::new("created_at");
            for &(entity_id, ts) in &entries {
                index.insert(entity_id, ts);
            }
            let pairs = index.serialize_to_kv_pairs().unwrap();
            let restored = RangeIndex::<u64>::load_from_kv_pairs("created_at", pairs.into_iter()).unwrap();

            // All values in original must be present in restored.
            for &(entity_id, ts) in &entries {
                let orig_bm = index.range(
                    Bound::Included(&ts),
                    Bound::Included(&ts),
                );
                let rest_bm = restored.range(
                    Bound::Included(&ts),
                    Bound::Included(&ts),
                );
                prop_assert!(orig_bm.contains(entity_id), "entity missing in original");
                prop_assert!(rest_bm.contains(entity_id), "entity missing in restored after roundtrip");
            }
            prop_assert_eq!(index.total_count(), restored.total_count());
        }
    }
}
