//! Searchable encryption for JSON values.
//!
//! This module implements the indexing scheme used by CipherStash for encrypted JSON
//! columns. Each leaf in a JSON document is flattened into a path/value pair, and
//! every path is tokenized with a keyed MAC so that paths and values can be matched
//! under encryption without revealing the underlying JSON.
//!
//! The resulting structures support three styles of query against an encrypted column:
//!
//! - **Containment** (`@>`): generate an [`SteQueryVec`] via [`JsonIndexer::query`] and
//!   compare it against a stored [`SteVec`].
//! - **Path projection** (`->`/`->>`): generate a [`TokenizedSelector`] via
//!   [`JsonIndexer::generate_selector`] to look up the ciphertext at a given JSON path.
//! - **Term comparison** (`=`, `<`, `>`): generate an [`EncryptedSteVecTerm`] via the
//!   `SteVecTerm` query op to compare a leaf value against a query value (uses ORE
//!   in the default `Standard` mode, OPE in `Compat` mode).
//!
//! See [`JsonIndexer`] for the main entry point.

mod path_values;
mod prefix_mac;
mod ste_vec;

use path_values::{flatmap_json, PathValue};
// use path_values::{PathTargets, PathValue};
use prefix_mac::{Blake3PrefixMac, HmacSha256PrefixMac, NewPrefixMac};
use serde_json::Value;
use ste_vec::{
    priv_state::{CompatTermBuilder, OrderableTerm, StandardTermBuilder},
    StePlaintextVec,
};
use zerokms_protocol::cipherstash_config::column;
use zerokms_protocol::cipherstash_config::column::{ArrayIndexMode, SteVecMode};

use super::{
    builder::StorageBuilder,
    indexer::{IndexerInit, Indexes, IndexesForQuery, QueryOp},
    IndexTerm, Plaintext, QueryBuilder, TryFromPlaintext,
};
use crate::{
    ejsonpath::Selector,
    encryption::{text::TokenFilter, EncryptionError},
    zerokms::IndexKey,
};

// Exports
pub use ste_vec::{
    EncryptedEntry, EncryptedSteVecTerm, EncryptedSteVecTermCompat, EncryptedSteVecTermStandard,
    SteQueryVec, SteVec, SteVecPendingEncryption, TokenizedSelector,
};

/// Configuration for a [`JsonIndexer`].
///
/// `JsonIndexerOptions` controls how a JSON document is tokenized and indexed.
/// It can be constructed directly, or derived from a column configuration via
/// `TryFrom<&column::IndexType>`.
#[derive(Default, Debug)]
pub struct JsonIndexerOptions {
    /// Domain-separation prefix mixed into the keyed MAC used to tokenize JSON
    /// paths and values.
    ///
    /// Two indexers with the same key but different prefixes will produce
    /// disjoint token spaces, so the prefix must match between the indexing
    /// side and the query side for results to be comparable. In practice this
    /// is set to a stable column identifier (e.g. `"cs_ste_vec_v1"`).
    pub prefix: String,
    /// Term filters applied to every string leaf node before indexing.
    ///
    /// Filters run in order against each string value (e.g. `Downcase`,
    /// `Stemmer`, `Stop`). If any filter rejects a term by returning `None`,
    /// the string is replaced with `Value::Null` rather than indexed. To match
    /// at query time, the same filters must be applied to the query plaintext.
    pub term_filters: Vec<TokenFilter>,
    /// Controls which selectors are emitted for array entries.
    ///
    /// See [`ArrayIndexMode`] — `ALL` emits item, positional, and wildcard
    /// selectors for each element; narrower modes (`ITEM`, `POSITION`,
    /// `WILDCARD`, `NONE`) reduce index size at the cost of which query
    /// shapes are supported.
    pub array_index_mode: ArrayIndexMode,
    /// Selects the STE-vec encoding scheme used by this indexer.
    ///
    /// See [`SteVecMode`] — the mode selects the orderable-term primitive:
    /// `Standard` (the default) uses ORE, `Compat` uses OPE. Selectors are
    /// always Blake3-keyed and term-side MACs are always HMAC-SHA256 in both
    /// modes. Indexes produced under different modes are not cross-comparable,
    /// so the indexing side and the query side must agree on the mode for
    /// results to match.
    pub mode: SteVecMode,
}

impl IndexerInit for JsonIndexer {
    type Args = JsonIndexerOptions;
    type Error = EncryptionError;

    fn try_init<A>(args: A) -> Result<Self, Self::Error>
    where
        Self::Args: TryFrom<A, Error = Self::Error>,
    {
        let args = JsonIndexerOptions::try_from(args)?;
        Ok(Self::new(args))
    }
}

impl<'k> Indexes<'k, Plaintext> for JsonIndexer {
    fn index(
        &self,
        mut builder: StorageBuilder<'k, Plaintext>,
    ) -> Result<StorageBuilder<'k, Plaintext>, EncryptionError> {
        let json = builder
            .plaintext()
            .clone_as_json()
            .ok_or(EncryptionError::IndexingError(
                "Failed to convert plaintext to JSON".to_string(),
            ))?;
        let index_key = builder.index_key();

        // TODO: consume value and builder together for the index method (should avoid the clone)
        let ste_vec_pending_encryption = self.index(json, index_key)?;
        builder.set_ste_vec(ste_vec_pending_encryption); // Probably should be try_add_ste_vec

        Ok(builder)
    }
}

impl<C> IndexesForQuery<Plaintext, C> for JsonIndexer {
    fn query_index(
        &self,
        builder: QueryBuilder<Plaintext, C>,
        op: QueryOp,
    ) -> Result<IndexTerm, EncryptionError> {
        let index_key = builder.index_key();
        match op {
            QueryOp::Default => {
                let json =
                    builder
                        .plaintext()
                        .clone_as_json()
                        .ok_or(EncryptionError::IndexingError(
                            "Failed to convert plaintext to JSON".to_string(),
                        ))?;
                self.query(json, index_key).map(IndexTerm::SteQueryVec)
            }
            QueryOp::SteVecSelector => {
                let path: String = String::try_from_plaintext(builder.plaintext().clone())?;
                let selector = Selector::parse(&path)?;
                let tokenized_selector = self.generate_selector(selector, index_key);
                Ok(IndexTerm::SteVecSelector(tokenized_selector))
            }
            QueryOp::SteVecTerm => self
                .generate_term(builder.plaintext(), index_key)
                .map(IndexTerm::SteVecTerm),
        }
    }
}

impl Default for JsonIndexer {
    fn default() -> Self {
        Self::new(Default::default())
    }
}

impl TryFrom<&column::IndexType> for JsonIndexerOptions {
    type Error = EncryptionError;

    fn try_from(value: &column::IndexType) -> Result<Self, Self::Error> {
        match value {
            column::IndexType::SteVec {
                prefix,
                term_filters,
                array_index_mode,
                mode,
            } => Ok(Self {
                prefix: prefix.clone(),
                term_filters: term_filters.iter().copied().map(From::from).collect(),
                array_index_mode: *array_index_mode,
                mode: *mode,
            }),
            _ => Err(EncryptionError::IndexingError(
                "JsonIndexerOptions can only be created from a SteVec index configuration"
                    .to_string(),
            )),
        }
    }
}

/// Indexer for searchable encryption of JSON documents.
///
/// A `JsonIndexer` flattens a JSON value into path/value pairs, applies any
/// configured term filters, and produces an [`SteVecPendingEncryption`] ready
/// to be sealed with a data key. The same indexer (with the same key and
/// prefix) is used on the query side to produce [`SteQueryVec`],
/// [`TokenizedSelector`], and [`EncryptedSteVecTerm`] values that match the
/// stored index.
///
/// Construct one with [`JsonIndexer::new`] from [`JsonIndexerOptions`], or use
/// [`Default`] for an indexer with an empty prefix, no term filters, and
/// `ArrayIndexMode::ALL`.
pub struct JsonIndexer {
    prefix: Vec<u8>,
    term_filters: Vec<TokenFilter>,
    array_index_mode: ArrayIndexMode,
    mode: SteVecMode,
}

impl JsonIndexer {
    /// Create a new indexer from the given options.
    pub fn new(opts: JsonIndexerOptions) -> Self {
        Self {
            prefix: Vec::from(opts.prefix.as_bytes()),
            term_filters: opts.term_filters,
            array_index_mode: opts.array_index_mode,
            mode: opts.mode,
        }
    }

    /// Generates an [`SteVecPendingEncryption`] from a JSON value.
    /// This represents the indexed form of the JSON, but with source plaintexts still present.
    /// This can then be encrypted into a final [`SteVec`] using a [`crate::zerokms::DataKeyWithTag`] with [`SteVecPendingEncryption::encrypt`].
    ///
    /// Once encrypted, the resulting [`SteVec`] can be stored in a database column or some other storage.
    ///
    /// # Example
    /// ```
    /// use cipherstash_client::encryption::{JsonIndexer, JsonIndexerOptions, SteVec};
    /// use cipherstash_client::zerokms::{DataKey, DataKeyWithTag, IndexKey};
    /// use zerokms_protocol::cipherstash_config::column::ArrayIndexMode;
    /// use serde_json::Value;
    ///
    /// let opts = JsonIndexerOptions {
    ///     prefix: "foo".to_string(),
    ///     term_filters: Vec::new(),
    ///     array_index_mode: ArrayIndexMode::ALL,
    ///     ..Default::default()
    /// };
    /// let indexer = JsonIndexer::new(opts);
    /// let index_key = IndexKey::from([0; 32]);
    /// let json = serde_json::json!({ "a": 1, "b": { "c": 2 } });
    /// let pending_encryption = indexer.index(json, &index_key).unwrap();
    ///
    /// // Encrypt the pending entries with a DataKeyWithTag
    /// // CAUTION: Always use a data key generated by Zerokms for production use.
    /// // Attempting to generate your own keys will result in invalid tags and decryption failures.
    /// let key = DataKey {
    ///     iv: [0; 16],
    ///     key: [0; 32],
    /// };
    /// let key_with_tag = DataKeyWithTag { key, tag: vec![0, 1, 2], decryption_policy: None };
    /// let ste_vec: SteVec<16> = pending_encryption.encrypt(key_with_tag).unwrap();
    /// ```
    ///
    pub fn index(
        &self,
        json: Value,
        index_key: &IndexKey,
    ) -> Result<SteVecPendingEncryption<16>, EncryptionError> {
        // Mode dispatch picks the orderable-term primitive (OPE for Compat,
        // ORE for Standard) via the TermBuilder type parameter. Selectors are
        // ALWAYS Blake3 and term-side MACs are ALWAYS HMAC-SHA256 — these
        // don't vary by mode.
        match self.mode {
            SteVecMode::Compat => self.index_with::<CompatTermBuilder>(json, index_key),
            SteVecMode::Standard => self.index_with::<StandardTermBuilder>(json, index_key),
        }
    }

    fn index_with<TB>(
        &self,
        json: Value,
        index_key: &IndexKey,
    ) -> Result<SteVecPendingEncryption<16>, EncryptionError>
    where
        TB: ste_vec::priv_state::TermBuilder,
    {
        let mut selector_macca = Blake3PrefixMac::new_prefix_mac(index_key, self.prefix.clone());
        let mut term_macca = HmacSha256PrefixMac::new_prefix_mac(index_key, self.prefix.clone());

        let processed = self.preprocess(json);
        let path_values: Vec<PathValue> =
            flatmap_json(&processed, self.array_index_mode, |path, value| {
                PathValue(path.to_vec(), value)
            });

        StePlaintextVec::from_iter(path_values)
            .index::<16, TB, _, _>(&mut selector_macca, &mut term_macca)
    }

    /// Applies any configured term filters to the JSON value recursively.
    ///
    /// Only string leaf nodes are processed. For each string value, the configured
    /// `term_filters` are applied in order. The filters are evaluated sequentially
    /// using a fallible fold (`try_fold`): if any filter rejects the term by
    /// returning `None`, processing stops and the entire string value is replaced
    /// with `null` in the resulting JSON.
    fn preprocess(&self, value: Value) -> Value {
        match value {
            Value::String(s) => {
                let mapped = self
                    .term_filters
                    .iter()
                    .try_fold(s, |val, filter| filter.process_single(val));

                // If any term filter returns None, the entire string is replaced with Null
                mapped.map(Value::String).unwrap_or(Value::Null)
            }
            Value::Array(arr) => {
                let v: Vec<Value> = arr.into_iter().map(|v| self.preprocess(v)).collect();
                Value::Array(v)
            }
            Value::Object(obj) => {
                let m = obj
                    .into_iter()
                    .map(|(k, v)| (k, self.preprocess(v)))
                    .collect();
                Value::Object(m)
            }
            v => v,
        }
    }

    /// Generate an [`SteQueryVec`] from a JSON value.
    ///
    /// This is useful for building containment queries (e.g. `@>` operator in Postgres).
    /// For example, given an [`SteVec`] column `attrs`, an [`SteQueryVec`] generated from a plaintext JSON value.
    ///
    /// ```
    /// use cipherstash_client::encryption::{JsonIndexer, JsonIndexerOptions};
    /// use cipherstash_client::zerokms::IndexKey;
    /// use zerokms_protocol::cipherstash_config::column::ArrayIndexMode;
    ///
    /// let opts = JsonIndexerOptions {
    ///     prefix: "foo".to_string(),
    ///     term_filters: Vec::new(),
    ///     array_index_mode: ArrayIndexMode::ALL,
    ///     ..Default::default()
    /// };
    /// let indexer = JsonIndexer::new(opts);
    /// let index_key = IndexKey::from([0; 32]);
    /// let json = serde_json::json!({ "a": 1, "b": { "c": 2 } });
    /// let q = indexer.query(json, &index_key).unwrap();
    /// ```
    ///
    /// This can then be used in a query like:
    ///
    /// ```sql
    /// -- $1 is the query parameter, q
    /// -- Example: q = [["aaa...", "bbb..."], ["ccc...", "ddd..."]]
    /// SELECT * FROM table WHERE attrs @> $1;
    /// ```
    ///
    pub fn query(
        &self,
        json: Value,
        index_key: &IndexKey,
    ) -> Result<SteQueryVec<16>, EncryptionError> {
        self.index(json, index_key).map(SteQueryVec::from)
    }

    /// Generate a [`TokenizedSelector`] from a JSON path.
    /// This is useful for building queries that target specific paths in a JSON document.
    ///
    /// For example, given an [`SteVec`] column `attrs`, a [`TokenizedSelector`] generated from a JSON path.
    /// ```
    /// use cipherstash_client::encryption::{JsonIndexer, JsonIndexerOptions};
    /// use cipherstash_client::zerokms::IndexKey;
    /// use cipherstash_client::ejsonpath::Selector;
    /// use zerokms_protocol::cipherstash_config::column::ArrayIndexMode;
    ///
    /// let opts = JsonIndexerOptions { prefix: "foo".to_string(), term_filters: Vec::new(), array_index_mode: ArrayIndexMode::ALL, ..Default::default() };
    /// let indexer = JsonIndexer::new(opts);
    /// let index_key = IndexKey::from([0; 32]);
    /// let json = serde_json::json!({ "a": 1, "b": { "c": 2 } });
    /// let selector = Selector::parse("$.b.c").unwrap();
    /// let tokenized_selector = indexer.generate_selector(selector, &index_key);
    /// ```
    ///
    /// This can then be used in a query like:
    ///
    /// ```sql
    /// -- $1 is the tokenized selector
    /// -- Example: "4eb62fb72d75cb53a309b3b091923daf"
    /// SELECT jsonb_path_query(attrs, '$ ? (exists(@ ? (@[0] == $1)))[2]') FROM table;
    /// ```
    ///
    /// This is equivalent to the unencrypted query:
    ///
    /// ```sql
    /// SELECT attrs->'b'->'c' FROM table WHERE attrs->'b'->'c' IS NOT NULL;
    /// ```
    ///
    pub fn generate_selector(
        &self,
        selector: Selector,
        index_key: &IndexKey,
    ) -> TokenizedSelector<16> {
        // Selectors are ALWAYS produced via Blake3, regardless of SteVecMode.
        // The mode only controls the term-side primitive (ORE vs OPE for
        // orderable terms; both modes share HMAC for MAC terms). Distinguishing
        // selectors by mode would needlessly partition the token space.
        let mut macca = Blake3PrefixMac::new_prefix_mac(index_key, self.prefix.clone());
        selector.tokenize(&mut macca)
    }

    /// Build an [`EncryptedSteVecTerm`] for a single plaintext leaf value.
    ///
    /// Used to construct the right-hand side of term-comparison queries
    /// (`=`, `<`, `>`) against a specific JSON path. The same prefix and
    /// index key must be used as for the stored index. The concrete
    /// [`EncryptedSteVecTerm`] variant is picked from `self.mode`: `Compat`
    /// yields `EncryptedSteVecTerm::Compat` (OPE), `Standard` yields
    /// `EncryptedSteVecTerm::Standard` (ORE).
    fn generate_term(
        &self,
        plaintext: &Plaintext,
        index_key: &IndexKey,
    ) -> Result<EncryptedSteVecTerm, EncryptionError> {
        // Term-side macca is ALWAYS HMAC-SHA256, regardless of mode. Mode only
        // chooses the orderable primitive (OPE for Compat, ORE for Standard).
        let mut macca = HmacSha256PrefixMac::new_prefix_mac(index_key, self.prefix.clone());
        let term = OrderableTerm::try_from(plaintext)?;
        match self.mode {
            SteVecMode::Compat => term
                .build_compat(&mut macca)
                .map(EncryptedSteVecTerm::Compat),
            SteVecMode::Standard => term
                .build_standard(&mut macca)
                .map(EncryptedSteVecTerm::Standard),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        ste_vec::{
            priv_state::OrderableTerm, EncryptedEntry, EncryptedSteVecTerm,
            EncryptedSteVecTermCompat, EncryptedSteVecTermStandard, QueryEntry, SteQueryVec,
            TokenizedSelector,
        },
        SteVec,
    };
    use crate::{
        ejsonpath::Selector,
        encryption::{text::TokenFilter, Plaintext},
        zerokms::{decrypt, DataKeyWithTag, EncryptedRecord, IndexKey},
    };
    use serde_json::{json, Value};
    use zerokms_protocol::cipherstash_config::column::SteVecMode;

    /// Simulates the built in `jsonb_array_elements` function in Postgres when applied to an SteVec.
    fn simulated_jsonb_array_elements(
        ste_vec: SteVec<16>,
    ) -> impl Iterator<Item = EncryptedEntry<16>> {
        ste_vec.into_iter()
    }

    /// Simulates the built in `@>` operator in Postgres when applied to an SteVec and a query.
    ///
    /// Equivalent to:
    ///
    /// ```sql
    /// select '[[1,2,3],[4,5,6]]'::jsonb @> '[[1,2],[4,5]]'::jsonb;
    /// ```
    ///
    /// Where 1 and 4 are the selectors and 2 and 5 are the terms.
    fn simulated_contains(ste_vec: SteVec<16>, query: SteQueryVec<16>) -> bool {
        // All query entries must be contained in the ste_vec
        query.0.iter().all(|query_entry| {
            ste_vec
                .0
                .iter()
                .any(|ste_entry| ste_entry.contains(query_entry))
        })
    }

    fn simulated_cs_ste_term_v1(
        ste_vec: SteVec<16>,
        selector: TokenizedSelector<16>,
    ) -> EncryptedSteVecTerm {
        simulated_jsonb_array_elements(ste_vec)
            .find(|entry| entry.tokenized_selector == selector)
            .expect("Selector not found in SteVec")
            .term
    }

    fn simulated_cs_ste_value_v1(
        ste_vec: SteVec<16>,
        selector: TokenizedSelector<16>,
    ) -> EncryptedRecord {
        simulated_jsonb_array_elements(ste_vec)
            .find(|entry| entry.tokenized_selector == selector)
            .unwrap()
            .record
    }

    /// Gets _all_ matching values for a given selector and returns as an array (which could be empty)
    fn simulated_cs_ste_values_v1(
        ste_vec: SteVec<16>,
        selector: TokenizedSelector<16>,
    ) -> Vec<EncryptedRecord> {
        simulated_jsonb_array_elements(ste_vec)
            .filter(|entry| entry.tokenized_selector == selector)
            .map(|entry| entry.record)
            .collect()
    }

    /// Converts the ste_vec into a list of query entries suitable for use in containment queries.
    ///
    /// ```sql
    /// SELECT * FROM table WHERE cs_ste_vec_v1(attrs) @> cs_ste_query_v1(stvec);
    /// ```
    fn simulated_cs_ste_query_v1(ste_vec: SteVec<16>) -> SteQueryVec<16> {
        SteQueryVec(
            simulated_jsonb_array_elements(ste_vec)
                .map(|entry| QueryEntry(entry.tokenized_selector, entry.term))
                .collect(),
        )
    }

    // -- Test helpers --
    use zerokms_protocol::cipherstash_config::column::ArrayIndexMode;

    fn gen_stevec(json: Value) -> SteVec<16> {
        gen_stevec_with_term_filters(json, Vec::new())
    }

    fn gen_stevec_with_term_filters(json: Value, term_filters: Vec<TokenFilter>) -> SteVec<16> {
        gen_stevec_with_options(json, term_filters, ArrayIndexMode::ALL)
    }

    fn gen_stevec_with_options(
        json: Value,
        term_filters: Vec<TokenFilter>,
        array_index_mode: ArrayIndexMode,
    ) -> SteVec<16> {
        let index_key = IndexKey::from([0; 32]);
        let data_key = DataKeyWithTag::default();

        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters,
            array_index_mode,
            ..Default::default()
        });
        indexer
            .index(json, &index_key)
            .expect("Failed to index JSON")
            .encrypt(data_key)
            .expect("Encryption failed")
    }

    fn gen_selector(selector: Selector) -> TokenizedSelector<16> {
        let index_key = IndexKey::from([0; 32]);
        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters: Vec::new(),
            array_index_mode: ArrayIndexMode::ALL,
            ..Default::default()
        });
        indexer.generate_selector(selector, &index_key)
    }

    fn gen_selector_with_mode(selector: Selector, mode: SteVecMode) -> TokenizedSelector<16> {
        let index_key = IndexKey::from([0; 32]);
        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters: Vec::new(),
            array_index_mode: ArrayIndexMode::ALL,
            mode,
        });
        indexer.generate_selector(selector, &index_key)
    }

    fn gen_term(plaintext: impl Into<Plaintext>) -> EncryptedSteVecTerm {
        let plaintext: Plaintext = plaintext.into();
        let index_key = IndexKey::from([0; 32]);
        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters: Vec::new(),
            array_index_mode: ArrayIndexMode::ALL,
            ..Default::default()
        });
        indexer
            .generate_term(&plaintext, &index_key)
            .expect("Failed to generate term")
    }

    fn gen_term_with_mode(
        plaintext: impl Into<Plaintext>,
        mode: SteVecMode,
    ) -> EncryptedSteVecTerm {
        let plaintext: Plaintext = plaintext.into();
        let index_key = IndexKey::from([0; 32]);
        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters: Vec::new(),
            array_index_mode: ArrayIndexMode::ALL,
            mode,
        });
        indexer
            .generate_term(&plaintext, &index_key)
            .expect("Failed to generate term")
    }

    fn gen_stevec_with_mode(json: Value, mode: SteVecMode) -> SteVec<16> {
        let index_key = IndexKey::from([0; 32]);
        let data_key = DataKeyWithTag::default();
        let indexer = super::JsonIndexer::new(super::JsonIndexerOptions {
            prefix: "cs_ste_vec_v1".to_string(),
            term_filters: Vec::new(),
            array_index_mode: ArrayIndexMode::ALL,
            mode,
        });
        indexer
            .index(json, &index_key)
            .expect("Failed to index JSON")
            .encrypt(data_key)
            .expect("Encryption failed")
    }

    fn decrypt_as_json(ciphertext: EncryptedRecord) -> Value {
        let decrypted = decrypt(ciphertext, Default::default()).expect("Decryption failed");
        let plaintext = Plaintext::from_slice(&decrypted).expect("Plaintext to be valid");
        plaintext.clone_as_json().expect("Plaintext to be JSON")
    }

    fn decrypt_vec_as_json(ciphertext: Vec<EncryptedRecord>) -> Vec<Value> {
        ciphertext.into_iter().map(decrypt_as_json).collect()
    }

    /// `foo->bar` when there is only one result
    fn stabby_single(json: &Value, path: &str) -> Value {
        let ste_vec = gen_stevec(json.clone());
        let ciphertext =
            simulated_cs_ste_value_v1(ste_vec, gen_selector(Selector::parse(path).unwrap()));
        decrypt_as_json(ciphertext)
    }

    /// `foo->bar` when there are multiple results
    fn stabby_collection(json: &Value, path: &str) -> Vec<Value> {
        let ste_vec = gen_stevec(json.clone());
        let ciphertexts =
            simulated_cs_ste_values_v1(ste_vec, gen_selector(Selector::parse(path).unwrap()));
        decrypt_vec_as_json(ciphertexts)
    }

    /// Like [stabby_single] but returns an [EncryptedTerm] instead of decrypting a value
    fn stabby_single_term(json: &Value, path: &str) -> EncryptedSteVecTerm {
        let ste_vec = gen_stevec(json.clone());
        simulated_cs_ste_term_v1(ste_vec, gen_selector(Selector::parse(path).unwrap()))
    }

    fn sample_json() -> Value {
        json!({
            "name": "John",
            "age": 30,
            "cars": [
                "Ford",
                "BMW",
                "Fiat",
                { "Tesla": "Model S" }
            ]
        })
    }

    #[test]
    fn test_stabby_arrow() {
        let json = sample_json();

        assert_eq!(
            stabby_single(&json, "$.cars"),
            json!([
                "Ford",
                "BMW",
                "Fiat",
                { "Tesla": "Model S" }
            ])
        );

        assert_eq!(stabby_single(&json, "$.name"), json!("John"));
        assert_eq!(stabby_single(&json, "$.age"), json!(30));
        assert_eq!(stabby_single(&json, "$.cars[*].Tesla"), json!("Model S"));
    }

    #[test]
    fn test_stabby_arrow_nested_array_elems() {
        let json = json!({
            "name": "John",
            "age": 30,
            "cars": [
                "Ford",
                "BMW",
                "Fiat",
                { "Tesla": "Model S" }
            ]
        });

        let results = stabby_collection(&json, "$.cars[*]");
        assert_eq!(results.len(), 4);

        assert_eq!(results[0], json!("Ford"));
        assert_eq!(results[1], json!("BMW"));
        assert_eq!(results[2], json!("Fiat"));
        assert_eq!(results[3], json!({ "Tesla": "Model S" }));
    }

    /// Return the whole JSON object
    #[test]
    fn test_stabby_arrow_root() {
        let json = sample_json();

        assert_eq!(stabby_single(&json, "$"), json);
    }

    #[test]
    fn test_stabby_arrow_nested_map() {
        let json = json!({
            "name": "John",
            "attrs": {
                "age": 30,
                "cars": [
                    "Ford",
                    "BMW",
                    "Fiat",
                    { "Tesla": "Model S" }
                ]
            }
        });

        assert_eq!(
            stabby_single(&json, "$.attrs"),
            json!({
                "age": 30,
                "cars": [
                    "Ford",
                    "BMW",
                    "Fiat",
                    { "Tesla": "Model S" }
                ]
            })
        );

        assert_eq!(stabby_single(&json, "$.attrs.age"), json!(30));

        assert_eq!(
            stabby_single(&json, "$.attrs.cars[*].Tesla"),
            json!("Model S")
        );
    }

    #[test]
    fn test_containment() -> Result<(), Box<dyn std::error::Error>> {
        let stored = gen_stevec(json!({
            "name": "John",
            "age": 30,
            "cars": [
                "Ford",
                "BMW",
                "Fiat",
                { "Tesla": "Model S" }
            ]
        }));

        let query_param = gen_stevec(json!({
            "cars": [
                "Ford",
                "BMW",
                "Fiat",
                { "Tesla": "Model S" }
            ]
        }));

        let query = simulated_cs_ste_query_v1(query_param);
        assert!(simulated_contains(stored, query));

        Ok(())
    }

    #[test]
    fn test_containment_nested_array() -> Result<(), Box<dyn std::error::Error>> {
        let stored = gen_stevec(json!({
            "top": {
                "nested": ["a", "b", "c"]
            }
        }));

        let query_param = gen_stevec(json!({
            "top": {
                "nested": ["a"]
            }
        }));

        let query = simulated_cs_ste_query_v1(query_param);
        assert!(simulated_contains(stored, query));

        Ok(())
    }

    #[test]
    fn test_wildcard_array() -> Result<(), Box<dyn std::error::Error>> {
        let stored = gen_stevec(json!({
            "ary": ["a", "b", "c"]
        }));

        let path = "$.ary";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary = hex::encode(selector.as_bytes());

        let path = "$.ary[1]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_idx = hex::encode(selector.as_bytes());

        let path = "$.ary[*]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_wld = hex::encode(selector.as_bytes());

        let path = "$.ary[@]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_itm = hex::encode(selector.as_bytes());

        let selectors = stored
            .0
            .iter()
            .map(|e| hex::encode(e.tokenized_selector.as_bytes()))
            .collect::<Vec<String>>();

        // Contains three items with array selector
        let items = selectors.iter().filter(|s| **s == ary).count();
        assert_eq!(items, 1);

        let items = selectors.iter().filter(|s| **s == ary_idx).count();
        assert_eq!(items, 1);

        let items = selectors.iter().filter(|s| **s == ary_wld).count();
        assert_eq!(items, 3);

        let items = selectors.iter().filter(|s| **s == ary_itm).count();
        assert_eq!(items, 3);

        // THIS IS USEFUL FOR DEBUGGING
        // LEFT HERE FOR FUTURE SELF
        // for e in stored {
        //     let t = hex::encode(e.tokenized_selector.as_bytes());

        //     let ty = match t {
        //         a if a == ary => "ary",
        //         a if a == ary_wld => "ary_wld",
        //         a if a == ary_idx => "ary_idx",
        //         a if a == ary_itm => "ary_itm",
        //         _ => "other",
        //     };
        //     println!("{:?}/{:?}", ty, e.parent_is_array);
        // }

        Ok(())
    }

    #[test]
    fn test_object_array() -> Result<(), Box<dyn std::error::Error>> {
        let stored = gen_stevec(json!({
            "ary": ["a", {"b": 1}]
        }));

        let path = "$.ary";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary = hex::encode(selector.as_bytes());

        let path = "$.ary[1]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_idx = hex::encode(selector.as_bytes());

        let path = "$.ary[*]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_wld = hex::encode(selector.as_bytes());

        let path = "$.ary[@]";
        let selector = Selector::parse(path).unwrap();
        let selector = gen_selector(selector);
        let ary_itm = hex::encode(selector.as_bytes());

        // let selectors = stored
        //     .0
        //     .iter()
        //     .map(|e| hex::encode(e.tokenized_selector.as_bytes()))
        //     .collect::<Vec<String>>();

        // // Contains three items with array selector
        // let items = selectors.iter().filter(|s| **s == ary).count();
        // assert_eq!(items, 1);

        // let items = selectors.iter().filter(|s| **s == ary_idx).count();
        // assert_eq!(items, 1);

        // let items = selectors.iter().filter(|s| **s == ary_wld).count();
        // assert_eq!(items, 3);

        // let items = selectors.iter().filter(|s| **s == ary_itm).count();
        // assert_eq!(items, 3);

        // THIS IS USEFUL FOR DEBUGGING
        // LEFT HERE FOR FUTURE SELF
        for e in stored {
            let t = hex::encode(e.tokenized_selector.as_bytes());

            let ty = match t {
                a if a == ary => "ary",
                a if a == ary_wld => "ary_wld",
                a if a == ary_idx => "ary_idx",
                a if a == ary_itm => "ary_itm",
                _ => "other",
            };
            println!("{:?}/{:?}", ty, e.parent_is_array);
        }

        Ok(())
    }

    #[test]
    fn test_containment_only_top_level_key() -> Result<(), Box<dyn std::error::Error>> {
        let stored = gen_stevec(json!({
            "top": {
                "nested": ["a", "b", "c"]
            }
        }));

        let query_param = gen_stevec(json!({
            "top": {}
        }));

        let query = simulated_cs_ste_query_v1(query_param);
        assert!(simulated_contains(stored, query));

        Ok(())
    }

    /// Equivalent to:
    ///
    /// Simulates:
    ///
    /// ```sql
    /// SELECT * FROM table WHERE cs_ste_value_v1('$.name') = '<encrypted-value>';
    /// ```
    ///
    /// ```sql
    /// SELECT * FROM table WHERE attrs->'name' = 'John';
    /// ```
    #[test]
    fn test_stabby_arrow_term_compare_string() {
        let json = sample_json();

        assert_eq!(stabby_single_term(&json, "$.name"), gen_term("John"));
        assert_eq!(
            stabby_single_term(&json, "$.cars[*].Tesla"),
            gen_term("Model S")
        );

        // Not equal
        assert_ne!(stabby_single_term(&json, "$.name"), gen_term("Wrong"));
    }

    /// Equivalent to:
    /// JSON that has been pre-processed with a downcase filter on all string leaf nodes.
    #[test]
    fn test_stabby_arrow_term_compare_string_case_insensitive() {
        let json = json!({
            "name": "John",
            "b": [1, 2, 3]
        });
        let ste_vec = gen_stevec_with_term_filters(json, vec![TokenFilter::Downcase]);
        let test_term =
            simulated_cs_ste_term_v1(ste_vec, gen_selector(Selector::parse("$.name").unwrap()));
        // Matches lowercased term
        assert_eq!(test_term, gen_term("john"));

        // Does not match original case or other cases
        assert_ne!(test_term, gen_term("John"));
        assert_ne!(test_term, gen_term("JOHN"));
    }

    /// Equivalent to:
    ///
    /// Simulates:
    ///
    /// ```sql
    /// SELECT * FROM table WHERE cs_ste_value_v1('$.age') > '<encrypted-value>';
    /// ```
    ///
    /// ```sql
    /// SELECT * FROM table WHERE attrs->'age' > 10;
    /// ```
    #[test]
    fn test_stabby_arrow_term_compare_num() {
        let json = sample_json();

        // TODO: These tests only work for f64 values (not integers for some reason)
        assert!(stabby_single_term(&json, "$.age") > gen_term(10_f64));
        assert!(stabby_single_term(&json, "$.age") == gen_term(30_f64));
        assert!(stabby_single_term(&json, "$.age") < gen_term(40_f64));
    }

    /// Mirror of [`test_containment`] for `SteVecMode::Standard`: a query-side
    /// `SteVec` built from a subset of the stored document must round-trip
    /// through `simulated_cs_ste_query_v1` and report as contained. Exercises
    /// `Indexes` end-to-end in Standard mode.
    #[test]
    fn test_standard_containment_basic() {
        let stored = gen_stevec_with_mode(json!({"name": "John", "age": 30}), SteVecMode::Standard);
        let query_param = gen_stevec_with_mode(json!({"name": "John"}), SteVecMode::Standard);
        let query = simulated_cs_ste_query_v1(query_param);
        assert!(simulated_contains(stored, query));
    }

    /// Mirror of [`test_stabby_arrow`] for `SteVecMode::Standard`: looking up a
    /// path-encoded selector on the stored vector must return the leaf
    /// ciphertext that decrypts back to the original JSON value. Exercises
    /// `IndexesForQuery::SteVecSelector` in Standard mode.
    #[test]
    fn test_standard_stabby_arrow_selector_lookup() {
        let json = json!({"name": "John", "age": 30});
        let ste_vec = gen_stevec_with_mode(json.clone(), SteVecMode::Standard);

        let path = "$.name";
        let selector = gen_selector_with_mode(Selector::parse(path).unwrap(), SteVecMode::Standard);
        let ciphertext = simulated_cs_ste_value_v1(ste_vec, selector);
        assert_eq!(decrypt_as_json(ciphertext), json!("John"));
    }

    /// Mirror of [`test_stabby_arrow_term_compare_num`] for the default
    /// `SteVecMode::Compat` (OPE). The stored term at `$.age` is encoded via
    /// `OrderableTerm::build_compat` (numeric `u64` through CLLW OPE with a
    /// type-tag bit prepended to the plaintext); this test pins that the
    /// resulting lexicographic byte ordering of the `Ope` ciphertext preserves
    /// the numeric ordering of f64 plaintexts.
    #[test]
    fn test_compat_term_comparison_ordering() {
        let json = json!({"age": 30});
        let ste_vec = gen_stevec_with_mode(json.clone(), SteVecMode::Compat);
        let selector =
            gen_selector_with_mode(Selector::parse("$.age").unwrap(), SteVecMode::Compat);
        let term_at_path = simulated_cs_ste_term_v1(ste_vec, selector);

        // Compat::Ope ordering is lexicographic byte comparison of the
        // tagged-plaintext OPE ciphertext.
        assert!(term_at_path > gen_term_with_mode(10_f64, SteVecMode::Compat));
        assert!(term_at_path == gen_term_with_mode(30_f64, SteVecMode::Compat));
        assert!(term_at_path < gen_term_with_mode(40_f64, SteVecMode::Compat));
    }

    /// Domain-separation check for `Compat::Ope` on real indexer output: a
    /// numeric term and a string term must never compare equal, and every
    /// numeric term must sort strictly before every string term. This holds
    /// because `OrderableTerm` prepends a type-tag bit (`0` numeric, `1`
    /// string) to the plaintext bit stream before CLLW OPE — every numeric
    /// ciphertext therefore sorts below every string ciphertext, regardless
    /// of payload magnitude.
    #[test]
    fn test_compat_ope_number_and_string_domains_are_separated() {
        let numbers: Vec<EncryptedSteVecTerm> = [-1.0e300_f64, -1.0, 0.0, 30.0, 1.0e300]
            .into_iter()
            .map(gen_term)
            .collect();
        let strings: Vec<EncryptedSteVecTerm> = ["a", "John", "zzzzzzzz", "\u{0}", "\u{0}\u{0}"]
            .into_iter()
            .map(gen_term)
            .collect();

        for n in &numbers {
            for s in &strings {
                assert!(
                    n < s,
                    "every numeric Ope term must sort before every string Ope term"
                );
                assert_ne!(
                    n, s,
                    "numeric and string Ope terms must never compare equal"
                );
            }
        }
    }

    /// Hand-rolled f64 corpus exercising edges where the orderable encoding
    /// (sign-bit handling) is most likely to break: cross-sign pairs, exact
    /// zero, infinities, subnormals, and very large magnitudes. Quickcheck's
    /// random generator covers the middle of the distribution but is unlikely
    /// to hit these exact values often, so they're pinned explicitly.
    ///
    /// Exercises `Compat` mode (OPE). The contract under test is
    /// `partial_cmp` semantics: `+0.0` and `-0.0` are Equal (IEEE 754), and
    /// NaN is excluded. `total_cmp` would distinguish `+0.0` from `-0.0` at
    /// the bit level, but the orderable encoding intentionally collapses them
    /// — matching what range queries over numeric data want.
    #[test]
    fn test_compat_ope_preserves_f64_ordering_edge_cases() {
        let corpus: &[f64] = &[
            f64::NEG_INFINITY,
            f64::MIN,
            -1.0e300,
            -1.0,
            -f64::MIN_POSITIVE,
            -0.0,
            0.0,
            f64::MIN_POSITIVE,
            1.0,
            1.0e300,
            f64::MAX,
            f64::INFINITY,
        ];

        for (i, &a) in corpus.iter().enumerate() {
            for &b in &corpus[i..] {
                let ope_a = gen_term_with_mode(a, SteVecMode::Compat);
                let ope_b = gen_term_with_mode(b, SteVecMode::Compat);

                let original = a
                    .partial_cmp(&b)
                    .expect("non-NaN f64 partial_cmp is defined");
                let encrypted = ope_a
                    .partial_cmp(&ope_b)
                    .expect("Compat::Ope terms must be comparable");

                assert_eq!(
                    original, encrypted,
                    "OPE ordering disagrees with f64::partial_cmp for a={a}, b={b}: \
                     f64 says {original:?}, OPE says {encrypted:?}"
                );
            }
        }
    }

    quickcheck::quickcheck! {
        /// For every non-NaN f64 pair, `Compat`-mode OPE ciphertext
        /// order must agree with `f64::partial_cmp` on the originals. NaN is
        /// discarded because IEEE 754 doesn't define an order on it.
        fn prop_compat_ope_preserves_f64_ordering(a: f64, b: f64) -> quickcheck::TestResult {
            if a.is_nan() || b.is_nan() {
                return quickcheck::TestResult::discard();
            }
            let ope_a = gen_term_with_mode(a, SteVecMode::Compat);
            let ope_b = gen_term_with_mode(b, SteVecMode::Compat);
            let original = a
                .partial_cmp(&b)
                .expect("non-NaN f64 partial_cmp is defined");
            let encrypted = match ope_a.partial_cmp(&ope_b) {
                Some(o) => o,
                None => return quickcheck::TestResult::failed(),
            };
            quickcheck::TestResult::from_bool(original == encrypted)
        }
    }

    /// Every term emitted by `JsonIndexer` in `Compat` mode — both
    /// query-side terms produced by `generate_term` and stored terms produced
    /// by `index` — must be wrapped in the `EncryptedSteVecTerm::Compat`
    /// variant, and Compat orderable terms are OPE-based: numbers and strings
    /// both collapse into the single `Ope` variant, separated by a domain-tag
    /// byte. On-disk payloads and downstream EQL consumers depend on this
    /// invariant.
    #[test]
    fn test_json_indexer_emits_compat_term_variants() {
        // generate_term: f64 plaintext → Compat::Ope
        assert!(
            matches!(
                gen_term_with_mode(42_f64, SteVecMode::Compat),
                EncryptedSteVecTerm::Compat(EncryptedSteVecTermCompat::Ope(_))
            ),
            "expected Compat::Ope for f64 plaintext"
        );

        // generate_term: string plaintext → Compat::Ope
        assert!(
            matches!(
                gen_term_with_mode("hello", SteVecMode::Compat),
                EncryptedSteVecTerm::Compat(EncryptedSteVecTermCompat::Ope(_))
            ),
            "expected Compat::Ope for string plaintext"
        );

        // index: a JSON document covering every leaf kind. Every resulting
        // entry must use a Compat term, and we must observe both Compat
        // subvariants (Mac for bool/null/array/object/root, Ope for numbers
        // and strings).
        let ste_vec = gen_stevec_with_mode(
            json!({
                "string_leaf": "hello",
                "number_leaf": 42,
                "bool_leaf": true,
                "null_leaf": null,
                "array_leaf": ["a"],
                "object_leaf": { "k": "v" }
            }),
            SteVecMode::Compat,
        );

        let (mut saw_mac, mut saw_ope) = (false, false);
        for entry in &ste_vec.0 {
            match &entry.term {
                EncryptedSteVecTerm::Compat(EncryptedSteVecTermCompat::Mac(_)) => saw_mac = true,
                EncryptedSteVecTerm::Compat(EncryptedSteVecTermCompat::Ope(_)) => saw_ope = true,
                other => panic!("indexed entry must use Compat term variant, got {other:?}"),
            }
        }
        assert!(saw_mac, "expected at least one Compat::Mac entry");
        assert!(saw_ope, "expected at least one Compat::Ope entry");
    }

    /// Mirror of [`test_json_indexer_emits_compat_term_variants`] for `SteVecMode::Standard`.
    /// Every term emitted under Standard mode — both query-side terms produced
    /// by `generate_term` and stored terms produced by `index` — must be
    /// wrapped in the `EncryptedSteVecTerm::Standard` variant. Standard
    /// orderable terms are ORE-based: numbers and strings both collapse into
    /// the single `Ore` variant, so we must observe both Standard subvariants
    /// (`Mac` for bool/null/array/object/root, `Ore` for numbers and strings)
    /// in a representative JSON document.
    #[test]
    fn test_json_indexer_emits_standard_term_variants() {
        // generate_term: f64 plaintext → Standard::Ore
        assert!(
            matches!(
                gen_term_with_mode(42_f64, SteVecMode::Standard),
                EncryptedSteVecTerm::Standard(EncryptedSteVecTermStandard::Ore(_))
            ),
            "expected Standard::Ore for f64 plaintext"
        );

        // generate_term: string plaintext → Standard::Ore
        assert!(
            matches!(
                gen_term_with_mode("hello", SteVecMode::Standard),
                EncryptedSteVecTerm::Standard(EncryptedSteVecTermStandard::Ore(_))
            ),
            "expected Standard::Ore for string plaintext"
        );

        let ste_vec = gen_stevec_with_mode(
            json!({
                "string_leaf": "hello",
                "number_leaf": 42,
                "bool_leaf": true,
                "null_leaf": null,
                "array_leaf": ["a"],
                "object_leaf": { "k": "v" }
            }),
            SteVecMode::Standard,
        );

        let (mut saw_mac, mut saw_ore) = (false, false);
        for entry in &ste_vec.0 {
            match &entry.term {
                EncryptedSteVecTerm::Standard(EncryptedSteVecTermStandard::Mac(_)) => {
                    saw_mac = true
                }
                EncryptedSteVecTerm::Standard(EncryptedSteVecTermStandard::Ore(_)) => {
                    saw_ore = true
                }
                other => panic!("indexed entry must use Standard term variant, got {other:?}"),
            }
        }
        assert!(saw_mac, "expected at least one Standard::Mac entry");
        assert!(saw_ore, "expected at least one Standard::Ore entry");
    }

    /// Term-side MAC is always HMAC-SHA256, regardless of mode. We exercise
    /// this by indexing a JSON with leaves that produce `Mac` terms (a
    /// bool) under both modes and checking that the raw MAC bytes match —
    /// they would not if Compat were Blake3-keyed and Standard HMAC-keyed.
    #[test]
    fn test_term_side_mac_is_mode_independent() {
        let json = json!({"flag": true});
        let compat = gen_stevec_with_mode(json.clone(), SteVecMode::Compat);
        let standard = gen_stevec_with_mode(json, SteVecMode::Standard);

        let flag_selector =
            gen_selector_with_mode(Selector::parse("$.flag").unwrap(), SteVecMode::Compat);

        let compat_bytes = match simulated_cs_ste_term_v1(compat, flag_selector) {
            EncryptedSteVecTerm::Compat(EncryptedSteVecTermCompat::Mac(m)) => m.as_ref().to_vec(),
            other => panic!("expected Compat::Mac, got {other:?}"),
        };
        let standard_bytes = match simulated_cs_ste_term_v1(standard, flag_selector) {
            EncryptedSteVecTerm::Standard(EncryptedSteVecTermStandard::Mac(m)) => {
                m.as_ref().to_vec()
            }
            other => panic!("expected Standard::Mac, got {other:?}"),
        };

        assert_eq!(
            compat_bytes, standard_bytes,
            "term-side MAC bytes must match across modes — both should be HMAC-SHA256"
        );
    }

    /// Selectors are mode-independent: both `Compat` and `Standard` produce
    /// Blake3-tokenized selectors. A regression here would re-introduce the
    /// mode dispatch in `generate_selector` and silently partition the token
    /// space, breaking query-side / index-side comparability across modes.
    #[test]
    fn test_selectors_are_mode_independent() {
        let selector = || Selector::parse("$.name").unwrap();
        let compat = gen_selector_with_mode(selector(), SteVecMode::Compat);
        let standard = gen_selector_with_mode(selector(), SteVecMode::Standard);
        assert_eq!(
            compat, standard,
            "selectors must be Blake3-keyed regardless of SteVecMode"
        );
    }

    /// Guards against a regression where the mode field stops reaching the
    /// leaf builders: indexing the same plaintext under the same mode must
    /// yield byte-identical terms.
    #[test]
    fn test_same_mode_same_input_is_deterministic() {
        let a = gen_term_with_mode("hello", SteVecMode::Compat);
        let b = gen_term_with_mode("hello", SteVecMode::Compat);
        assert_eq!(a, b);

        let c = gen_term_with_mode("hello", SteVecMode::Standard);
        let d = gen_term_with_mode("hello", SteVecMode::Standard);
        assert_eq!(c, d);
    }

    /// The same plaintext under different modes must land in distinct outer
    /// `EncryptedSteVecTerm` variants and compare unequal.
    #[test]
    fn test_different_modes_produce_different_outer_variants() {
        let compat = gen_term_with_mode("hello", SteVecMode::Compat);
        let standard = gen_term_with_mode("hello", SteVecMode::Standard);

        assert!(matches!(compat, EncryptedSteVecTerm::Compat(_)));
        assert!(matches!(standard, EncryptedSteVecTerm::Standard(_)));
        // PartialEq across variants returns false.
        assert_ne!(compat, standard);
    }

    #[test]
    fn test_json_indexer_options_default_mode_is_standard() {
        use zerokms_protocol::cipherstash_config::column::SteVecMode;
        let opts = super::JsonIndexerOptions::default();
        assert_eq!(opts.mode, SteVecMode::Standard);
    }

    #[test]
    fn test_try_from_ste_vec_index_type_threads_mode_through() {
        use zerokms_protocol::cipherstash_config::column::{ArrayIndexMode, IndexType, SteVecMode};

        let it = IndexType::SteVec {
            prefix: "p".into(),
            term_filters: vec![],
            array_index_mode: ArrayIndexMode::default(),
            mode: SteVecMode::Standard,
        };
        let opts = super::JsonIndexerOptions::try_from(&it).unwrap();
        assert_eq!(opts.mode, SteVecMode::Standard);
    }

    #[test]
    fn test_root_record_as_object() {
        let json = json!({ "name": "John" });
        let ste_vec = gen_stevec(json.clone());
        let root = ste_vec.into_root_ciphertext().unwrap();

        assert_eq!(decrypt_as_json(root), json);
    }

    #[test]
    fn test_root_record_as_array() {
        let json = json!([1, 2, 3]);
        let ste_vec = gen_stevec(json.clone());
        let root = ste_vec.into_root_ciphertext().unwrap();

        assert_eq!(decrypt_as_json(root), json);
    }

    #[test]
    fn test_array_entries() {
        let json = json!(["A", "B", "C",]);

        let ste_vec = gen_stevec(json.clone());

        assert_eq!(10, ste_vec.0.len());

        assert!(!ste_vec.0[0].parent_is_array);
        assert!(ste_vec.0[1].parent_is_array);
        assert!(ste_vec.0[2].parent_is_array);
        assert!(!ste_vec.0[3].parent_is_array);
        assert!(ste_vec.0[4].parent_is_array);
        assert!(ste_vec.0[5].parent_is_array);
        assert!(!ste_vec.0[6].parent_is_array);

        let json = json!({"a": [
            "A",
            "B",
            "C",
        ]});

        let ste_vec = gen_stevec(json.clone());

        assert_eq!(11, ste_vec.0.len());

        assert!(!ste_vec.0[0].parent_is_array);
        assert!(!ste_vec.0[1].parent_is_array);
        assert!(ste_vec.0[2].parent_is_array);
        assert!(ste_vec.0[3].parent_is_array);
    }

    #[test]
    fn test_array_index_mode_none() {
        let json = json!(["A", "B", "C"]);
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::NONE);

        // With NONE: root(1) = 1 entry
        assert_eq!(1, ste_vec.0.len());
    }

    #[test]
    fn test_array_index_mode_wildcard_only() {
        let json = json!(["A", "B", "C"]);
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::WILDCARD);

        // With WILDCARD: root(1) + 3 wildcard = 4 entries
        assert_eq!(4, ste_vec.0.len());
    }

    #[test]
    fn test_array_index_mode_all_matches_original() {
        let json = json!(["A", "B", "C"]);
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::ALL);

        // With ALL: root(1) + 3*(item + position + wildcard) = 1 + 9 = 10 entries
        assert_eq!(10, ste_vec.0.len());
    }

    #[test]
    fn test_nested_object_with_array_mode_none() {
        let json = json!({
            "name": "John",
            "cars": ["Ford", "BMW"]
        });
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::NONE);

        // With NONE: root(1) + name(1) + cars(1) = 3 entries
        assert_eq!(3, ste_vec.0.len());
    }

    #[test]
    fn test_nested_object_with_array_mode_wildcard() {
        let json = json!({
            "name": "John",
            "cars": ["Ford", "BMW"]
        });
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::WILDCARD);

        // With WILDCARD: root(1) + name(1) + cars(1) + 2 wildcard = 5 entries
        assert_eq!(5, ste_vec.0.len());
    }

    #[test]
    fn test_array_index_mode_item_only() {
        let json = json!(["A", "B", "C"]);
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::ITEM);
        // With ITEM: root(1) + 3 item entries = 4
        assert_eq!(4, ste_vec.0.len());
    }

    #[test]
    fn test_array_index_mode_position_only() {
        let json = json!(["A", "B", "C"]);
        let ste_vec = gen_stevec_with_options(json, vec![], ArrayIndexMode::POSITION);
        // With POSITION: root(1) + 3 position entries = 4
        assert_eq!(4, ste_vec.0.len());
    }

    #[test]
    fn test_orderable_term_rejects_non_orderable_plaintexts() {
        let rejected: Vec<Plaintext> = vec![
            Plaintext::Boolean(Some(true)),
            Plaintext::Json(Some(serde_json::json!({"x": 1}))),
            Plaintext::Int(None), // null
        ];

        for p in &rejected {
            let err = OrderableTerm::try_from(p).expect_err("OrderableTerm should reject");
            assert!(
                matches!(err, crate::encryption::EncryptionError::InvalidValue(_)),
                "expected InvalidValue for {p:?}, got {err}"
            );
        }
    }

    mod preprocessing {
        use super::super::JsonIndexer;
        use serde_json::json;

        #[test]
        fn test_downcase_filter() {
            let indexer = JsonIndexer::new(super::super::JsonIndexerOptions {
                prefix: "test".to_string(),
                term_filters: vec![super::TokenFilter::Downcase],
                array_index_mode: super::super::ArrayIndexMode::ALL,
                ..Default::default()
            });

            let input = json!({
                "Name": "John DOE",
                "Details": {
                    "City": "New York",
                    "Bio": "Loves Programming"
                },
                "Tags": ["Rust", "Encryption", "JSON"]
            });

            let expected = json!({
                "Name": "john doe",
                "Details": {
                    "City": "new york",
                    "Bio": "loves programming"
                },
                "Tags": ["rust", "encryption", "json"]
            });

            let processed = indexer.preprocess(input);
            assert_eq!(processed, expected);
        }

        #[test]
        fn test_filter_stemmer() {
            let indexer = JsonIndexer::new(super::super::JsonIndexerOptions {
                prefix: "test".to_string(),
                term_filters: vec![super::TokenFilter::Stemmer],
                array_index_mode: super::super::ArrayIndexMode::ALL,
                ..Default::default()
            });

            let input = json!({
                "running": "running",
                "jumps": "jumps",
                "easily": "easily",
                "fairly": "fairly"
            });

            let expected = json!({
                "running": "run",
                "jumps": "jump",
                "easily": "easili",
                "fairly": "fair"
            });
            let processed = indexer.preprocess(input);
            assert_eq!(processed, expected);
        }

        // Not sure how useful this would be but the token filter exists
        // so testing it here
        #[test]
        fn test_filter_stop_words() {
            let indexer = JsonIndexer::new(super::super::JsonIndexerOptions {
                prefix: "test".to_string(),
                term_filters: vec![super::TokenFilter::Stop],
                array_index_mode: super::super::ArrayIndexMode::ALL,
                ..Default::default()
            });

            let input = json!({
                "a": "The",
                "b": "Something"
            });

            let expected = json!({
                "a": null,
                "b": "Something"
            });

            let processed = indexer.preprocess(input);
            assert_eq!(processed, expected);
        }
    }
}
