sentinel_dbms/
collection.rs

1use std::{path::PathBuf, sync::Arc};
2
3use async_stream::stream;
4use futures::{StreamExt as _, TryStreamExt as _};
5use serde_json::{json, Value};
6use tokio::fs as tokio_fs;
7use tokio_stream::Stream;
8use tracing::{debug, error, trace, warn};
9
10use crate::{
11    comparison::compare_values,
12    filtering::matches_filters,
13    projection::project_document,
14    query::{Aggregation, Filter},
15    streaming::stream_document_ids,
16    validation::{is_reserved_name, is_valid_document_id_chars},
17    Document,
18    Result,
19    SentinelError,
20};
21
22/// A collection represents a namespace for documents in the Sentinel database.
23///
24/// Collections are backed by filesystem directories, where each document is stored
25/// as a JSON file with metadata including version, timestamps, hash, and optional signature.
26/// The collection provides CRUD operations (Create, Read, Update, Delete) and advanced
27/// querying capabilities with streaming support for memory-efficient handling of large datasets.
28///
29/// # Structure
30///
31/// Each collection is stored in a directory with the following structure:
32/// - `{collection_name}/` - Root directory for the collection
33/// - `{collection_name}/{id}.json` - Individual document files with embedded metadata
34/// - `{collection_name}/.deleted/` - Soft-deleted documents (for recovery)
35/// - `{collection_name}/.metadata.json` - Collection metadata and indices (future)
36///
37/// # Streaming Operations
38///
39/// For memory efficiency with large datasets, operations like `filter()` and `query()`
40/// return async streams that process documents one-by-one rather than loading
41/// all documents into memory simultaneously.
42///
43/// # Example
44///
45/// ```rust
46/// use sentinel_dbms::{Store, Collection};
47/// use futures::TryStreamExt;
48/// use serde_json::json;
49///
50/// # async fn example() -> sentinel_dbms::Result<()> {
51/// // Create a store and get a collection
52/// let store = Store::new("/tmp/sentinel", None).await?;
53/// let collection = store.collection("users").await?;
54///
55/// // Insert a document
56/// let user_data = json!({
57///     "name": "Alice",
58///     "email": "alice@example.com",
59///     "age": 30
60/// });
61/// collection.insert("user-123", user_data).await?;
62///
63/// // Retrieve the document
64/// let doc = collection.get("user-123").await?;
65/// assert!(doc.is_some());
66/// assert_eq!(doc.unwrap().id(), "user-123");
67///
68/// // Stream all documents matching a predicate
69/// let adults = collection.filter(|doc| {
70///     doc.data().get("age")
71///         .and_then(|v| v.as_i64())
72///         .map_or(false, |age| age >= 18)
73/// });
74/// let adult_docs: Vec<_> = adults.try_collect().await?;
75/// assert_eq!(adult_docs.len(), 1);
76/// # Ok(())
77/// # }
78/// ```
79#[derive(Debug, Clone, PartialEq, Eq)]
80#[allow(
81    clippy::field_scoped_visibility_modifiers,
82    reason = "fields need to be pub(crate) for internal access"
83)]
84pub struct Collection {
85    /// The filesystem path to the collection directory.
86    pub(crate) path:        PathBuf,
87    /// The signing key for the collection.
88    pub(crate) signing_key: Option<Arc<sentinel_crypto::SigningKey>>,
89}
90
91#[allow(
92    unexpected_cfgs,
93    reason = "tarpaulin_include is set by code coverage tool"
94)]
95impl Collection {
96    /// Returns the name of the collection.
97    pub fn name(&self) -> &str { self.path.file_name().unwrap().to_str().unwrap() }
98
99    /// Inserts a new document into the collection or overwrites an existing one.
100    ///
101    /// The document is serialized to pretty-printed JSON and written to a file named
102    /// `{id}.json` within the collection's directory. If a document with the same ID
103    /// already exists, it will be overwritten.
104    ///
105    /// # Arguments
106    ///
107    /// * `id` - A unique identifier for the document. This will be used as the filename (with
108    ///   `.json` extension). Must be filesystem-safe.
109    /// * `data` - The JSON data to store. Can be any valid `serde_json::Value`.
110    ///
111    /// # Returns
112    ///
113    /// Returns `Ok(())` on success, or a `SentinelError` if the operation fails
114    /// (e.g., filesystem errors, serialization errors).
115    ///
116    /// # Example
117    ///
118    /// ```rust
119    /// use sentinel_dbms::{Store, Collection};
120    /// use serde_json::json;
121    ///
122    /// # async fn example() -> sentinel_dbms::Result<()> {
123    /// let store = Store::new("/path/to/data", None).await?;
124    /// let collection = store.collection("users").await?;
125    ///
126    /// let user = json!({
127    ///     "name": "Alice",
128    ///     "email": "alice@example.com",
129    ///     "age": 30
130    /// });
131    ///
132    /// collection.insert("user-123", user).await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn insert(&self, id: &str, data: Value) -> Result<()> {
137        trace!("Inserting document with id: {}", id);
138        validate_document_id(id)?;
139        let file_path = self.path.join(format!("{}.json", id));
140
141        #[allow(clippy::pattern_type_mismatch, reason = "false positive")]
142        let doc = if let Some(key) = &self.signing_key {
143            debug!("Creating signed document for id: {}", id);
144            Document::new(id.to_owned(), data, key).await?
145        }
146        else {
147            debug!("Creating unsigned document for id: {}", id);
148            Document::new_without_signature(id.to_owned(), data).await?
149        };
150
151        // COVERAGE BYPASS: The error! call in map_err (lines 147-148) is defensive code for
152        // serialization failures that cannot realistically occur with valid Document structs.
153        // Testing would require corrupting serde_json itself. Tarpaulin doesn't track map_err closures
154        // properly.
155        #[cfg(not(tarpaulin_include))]
156        let json = serde_json::to_string_pretty(&doc).map_err(|e| {
157            error!("Failed to serialize document {} to JSON: {}", id, e);
158            e
159        })?;
160
161        tokio_fs::write(&file_path, json).await.map_err(|e| {
162            error!(
163                "Failed to write document {} to file {:?}: {}",
164                id, file_path, e
165            );
166            e
167        })?;
168        debug!("Document {} inserted successfully", id);
169        Ok(())
170    }
171
172    /// Retrieves a document from the collection by its ID.
173    ///
174    /// Reads the JSON file corresponding to the given ID and deserializes it into
175    /// a `Document` struct. If the document doesn't exist, returns `None`.
176    ///
177    /// By default, this method verifies both hash and signature with strict mode.
178    /// Use `get_with_verification()` to customize verification behavior.
179    ///
180    /// # Arguments
181    ///
182    /// * `id` - The unique identifier of the document to retrieve.
183    ///
184    /// # Returns
185    ///
186    /// Returns:
187    /// - `Ok(Some(Document))` if the document exists and was successfully read
188    /// - `Ok(None)` if the document doesn't exist (file not found)
189    /// - `Err(SentinelError)` if there was an error reading or parsing the document
190    ///
191    /// # Example
192    ///
193    /// ```rust
194    /// use sentinel_dbms::{Store, Collection};
195    /// use serde_json::json;
196    ///
197    /// # async fn example() -> sentinel_dbms::Result<()> {
198    /// let store = Store::new("/path/to/data", None).await?;
199    /// let collection = store.collection("users").await?;
200    ///
201    /// // Insert a document first
202    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
203    ///
204    /// // Retrieve the document (with verification enabled by default)
205    /// let doc = collection.get("user-123").await?;
206    /// assert!(doc.is_some());
207    /// assert_eq!(doc.unwrap().id(), "user-123");
208    ///
209    /// // Try to get a non-existent document
210    /// let missing = collection.get("user-999").await?;
211    /// assert!(missing.is_none());
212    /// # Ok(())
213    /// # }
214    /// ```
215    pub async fn get(&self, id: &str) -> Result<Option<Document>> {
216        self.get_with_verification(id, &crate::VerificationOptions::default())
217            .await
218    }
219
220    /// Retrieves a document from the collection by its ID with custom verification options.
221    ///
222    /// Reads the JSON file corresponding to the given ID and deserializes it into
223    /// a `Document` struct. If the document doesn't exist, returns `None`.
224    ///
225    /// # Arguments
226    ///
227    /// * `id` - The unique identifier of the document to retrieve.
228    /// * `options` - Verification options controlling hash and signature verification.
229    ///
230    /// # Returns
231    ///
232    /// Returns:
233    /// - `Ok(Some(Document))` if the document exists and was successfully read
234    /// - `Ok(None)` if the document doesn't exist (file not found)
235    /// - `Err(SentinelError)` if there was an error reading, parsing, or verifying the document
236    ///
237    /// # Example
238    ///
239    /// ```rust
240    /// use sentinel_dbms::{Store, Collection, VerificationMode, VerificationOptions};
241    /// use serde_json::json;
242    ///
243    /// # async fn example() -> sentinel_dbms::Result<()> {
244    /// let store = Store::new("/path/to/data", None).await?;
245    /// let collection = store.collection("users").await?;
246    ///
247    /// // Insert a document first
248    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
249    ///
250    /// // Retrieve with warning mode instead of strict
251    /// let options = VerificationOptions {
252    ///     verify_signature: true,
253    ///     verify_hash: true,
254    ///     signature_verification_mode: VerificationMode::Warn,
255    ///     empty_signature_mode: VerificationMode::Warn,
256    ///     hash_verification_mode: VerificationMode::Warn,
257    /// };
258    /// let doc = collection.get_with_verification("user-123", &options).await?;
259    /// assert!(doc.is_some());
260    /// # Ok(())
261    /// # }
262    /// ```
263    pub async fn get_with_verification(
264        &self,
265        id: &str,
266        options: &crate::VerificationOptions,
267    ) -> Result<Option<Document>> {
268        trace!(
269            "Retrieving document with id: {} (verification enabled: {})",
270            id,
271            options.verify_signature || options.verify_hash
272        );
273        validate_document_id(id)?;
274        let file_path = self.path.join(format!("{}.json", id));
275        match tokio_fs::read_to_string(&file_path).await {
276            Ok(content) => {
277                debug!("Document {} found, parsing JSON", id);
278                let mut doc: Document = serde_json::from_str(&content).map_err(|e| {
279                    error!("Failed to parse JSON for document {}: {}", id, e);
280                    e
281                })?;
282                // Ensure the id matches the filename
283                doc.id = id.to_owned();
284
285                self.verify_document(&doc, options).await?;
286
287                trace!("Document {} retrieved successfully", id);
288                Ok(Some(doc))
289            },
290            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
291                debug!("Document {} not found", id);
292                Ok(None)
293            },
294            Err(e) => {
295                error!("IO error reading document {}: {}", id, e);
296                Err(SentinelError::Io {
297                    source: e,
298                })
299            },
300        }
301    }
302
303    /// Deletes a document from the collection (soft delete).
304    ///
305    /// Moves the JSON file corresponding to the given ID to a `.deleted/` subdirectory
306    /// within the collection. This implements soft deletes, allowing for recovery
307    /// of accidentally deleted documents. The `.deleted/` directory is created
308    /// automatically if it doesn't exist.
309    ///
310    /// If the document doesn't exist, the operation succeeds silently (idempotent).
311    ///
312    /// # Arguments
313    ///
314    /// * `id` - The unique identifier of the document to delete.
315    ///
316    /// # Returns
317    ///
318    /// Returns `Ok(())` on success (including when the document doesn't exist),
319    /// or a `SentinelError` if the operation fails due to filesystem errors.
320    ///
321    /// # Example
322    ///
323    /// ```rust
324    /// use sentinel_dbms::{Store, Collection};
325    /// use serde_json::json;
326    ///
327    /// # async fn example() -> sentinel_dbms::Result<()> {
328    /// let store = Store::new("/path/to/data", None).await?;
329    /// let collection = store.collection("users").await?;
330    ///
331    /// // Insert a document
332    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
333    ///
334    /// // Soft delete the document
335    /// collection.delete("user-123").await?;
336    ///
337    /// // Document is no longer accessible via get()
338    /// let doc = collection.get("user-123").await?;
339    /// assert!(doc.is_none());
340    ///
341    /// // But the file still exists in .deleted/
342    /// // (can be recovered manually if needed)
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub async fn delete(&self, id: &str) -> Result<()> {
347        trace!("Deleting document with id: {}", id);
348        validate_document_id(id)?;
349        let source_path = self.path.join(format!("{}.json", id));
350        let deleted_dir = self.path.join(".deleted");
351        let dest_path = deleted_dir.join(format!("{}.json", id));
352
353        // Check if source exists
354        match tokio_fs::metadata(&source_path).await {
355            Ok(_) => {
356                debug!("Document {} exists, moving to .deleted", id);
357                // Create .deleted directory if it doesn't exist
358                tokio_fs::create_dir_all(&deleted_dir).await.map_err(|e| {
359                    error!(
360                        "Failed to create .deleted directory {:?}: {}",
361                        deleted_dir, e
362                    );
363                    e
364                })?;
365                // Move file to .deleted/
366                tokio_fs::rename(&source_path, &dest_path)
367                    .await
368                    .map_err(|e| {
369                        error!("Failed to move document {} to .deleted: {}", id, e);
370                        e
371                    })?;
372                debug!("Document {} soft deleted successfully", id);
373                Ok(())
374            },
375            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
376                debug!(
377                    "Document {} not found, already deleted or never existed",
378                    id
379                );
380                Ok(())
381            },
382            Err(e) => {
383                error!("IO error checking document {} existence: {}", id, e);
384                Err(SentinelError::Io {
385                    source: e,
386                })
387            },
388        }
389    }
390
391    /// Lists all document IDs in the collection.
392    ///
393    /// Returns a stream of document IDs from the collection directory.
394    /// IDs are streamed as they are discovered, without guaranteed ordering.
395    /// For sorted results, collect the stream and sort manually.
396    ///
397    /// # Returns
398    ///
399    /// Returns a stream of document IDs (filenames without the .json extension),
400    /// or a `SentinelError` if the operation fails due to filesystem errors.
401    ///
402    /// # Example
403    ///
404    /// ```rust
405    /// use sentinel_dbms::{Store, Collection};
406    /// use serde_json::json;
407    /// use futures::TryStreamExt;
408    ///
409    /// # async fn example() -> sentinel_dbms::Result<()> {
410    /// let store = Store::new("/path/to/data", None).await?;
411    /// let collection = store.collection("users").await?;
412    ///
413    /// // Insert some documents
414    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
415    /// collection.insert("user-456", json!({"name": "Bob"})).await?;
416    ///
417    /// // Stream all document IDs
418    /// let ids: Vec<_> = collection.list().try_collect().await?;
419    /// assert_eq!(ids.len(), 2);
420    /// assert!(ids.contains(&"user-123".to_string()));
421    /// assert!(ids.contains(&"user-456".to_string()));
422    /// # Ok(())
423    /// # }
424    /// ```
425    pub fn list(&self) -> std::pin::Pin<Box<dyn Stream<Item = Result<String>> + Send>> {
426        trace!("Streaming document IDs from collection: {}", self.name());
427        stream_document_ids(self.path.clone())
428    }
429
430    /// Counts the total number of documents in the collection.
431    ///
432    /// This method streams through all document IDs and counts them efficiently
433    /// without loading the full documents into memory.
434    ///
435    /// # Returns
436    ///
437    /// Returns the total count of documents as a `usize`, or a `SentinelError` if
438    /// there was an error accessing the collection.
439    ///
440    /// # Example
441    ///
442    /// ```rust
443    /// use sentinel_dbms::{Store, Collection};
444    /// use serde_json::json;
445    ///
446    /// # async fn example() -> sentinel_dbms::Result<()> {
447    /// let store = Store::new("/path/to/data", None).await?;
448    /// let collection = store.collection("users").await?;
449    ///
450    /// // Insert some documents
451    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
452    /// collection.insert("user-456", json!({"name": "Bob"})).await?;
453    ///
454    /// // Count the documents
455    /// let count = collection.count().await?;
456    /// assert_eq!(count, 2);
457    /// # Ok(())
458    /// # }
459    /// ```
460    pub async fn count(&self) -> Result<usize> {
461        trace!("Counting documents in collection: {}", self.name());
462        let ids: Vec<String> = self.list().try_collect().await?;
463        Ok(ids.len())
464    }
465
466    /// Performs bulk insert operations on multiple documents.
467    ///
468    /// Inserts multiple documents into the collection in a single operation.
469    /// If any document fails to insert, the operation stops and returns an error.
470    /// Documents are inserted in the order provided.
471    ///
472    /// # Arguments
473    ///
474    /// * `documents` - A vector of (id, data) tuples to insert.
475    ///
476    /// # Returns
477    ///
478    /// Returns `Ok(())` on success, or a `SentinelError` if any operation fails.
479    /// In case of failure, some documents may have been inserted before the error.
480    ///
481    /// # Example
482    ///
483    /// ```rust
484    /// use sentinel_dbms::{Store, Collection};
485    /// use serde_json::json;
486    ///
487    /// # async fn example() -> sentinel_dbms::Result<()> {
488    /// let store = Store::new("/path/to/data", None).await?;
489    /// let collection = store.collection("users").await?;
490    ///
491    /// // Prepare bulk documents
492    /// let documents = vec![
493    ///     ("user-123", json!({"name": "Alice", "role": "admin"})),
494    ///     ("user-456", json!({"name": "Bob", "role": "user"})),
495    ///     ("user-789", json!({"name": "Charlie", "role": "user"})),
496    /// ];
497    ///
498    /// // Bulk insert
499    /// collection.bulk_insert(documents).await?;
500    ///
501    /// // Verify all documents were inserted
502    /// assert!(collection.get("user-123").await?.is_some());
503    /// assert!(collection.get("user-456").await?.is_some());
504    /// assert!(collection.get("user-789").await?.is_some());
505    /// # Ok(())
506    /// # }
507    /// ```
508    pub async fn bulk_insert(&self, documents: Vec<(&str, Value)>) -> Result<()> {
509        let count = documents.len();
510        trace!(
511            "Bulk inserting {} documents into collection {}",
512            count,
513            self.name()
514        );
515        for (id, data) in documents {
516            self.insert(id, data).await?;
517        }
518        debug!("Bulk insert of {} documents completed successfully", count);
519        Ok(())
520    }
521
522    /// Filters documents in the collection using a predicate function.
523    ///
524    /// This method performs streaming filtering by loading and checking documents
525    /// one by one, keeping only matching documents in memory. This approach
526    /// minimizes memory usage while maintaining good performance for most use cases.
527    ///
528    /// By default, this method verifies both hash and signature with strict mode.
529    /// Use `filter_with_verification()` to customize verification behavior.
530    ///
531    /// # Arguments
532    ///
533    /// * `predicate` - A function that takes a `&Document` and returns `true` if the document
534    ///   should be included in the results.
535    ///
536    /// # Returns
537    ///
538    /// Returns a stream of documents that match the predicate.
539    ///
540    /// # Example
541    ///
542    /// ```rust
543    /// use sentinel_dbms::{Store, Collection};
544    /// use serde_json::json;
545    /// use futures::stream::StreamExt;
546    ///
547    /// # async fn example() -> sentinel_dbms::Result<()> {
548    /// let store = Store::new("/path/to/data", None).await?;
549    /// let collection = store.collection("users").await?;
550    ///
551    /// // Insert some test data
552    /// collection.insert("user-1", json!({"name": "Alice", "age": 25})).await?;
553    /// collection.insert("user-2", json!({"name": "Bob", "age": 30})).await?;
554    ///
555    /// // Filter for users older than 26
556    /// let mut adults = collection.filter(|doc| {
557    ///     doc.data().get("age")
558    ///         .and_then(|v| v.as_i64())
559    ///         .map_or(false, |age| age > 26)
560    /// });
561    ///
562    /// let mut count = 0;
563    /// while let Some(doc) = adults.next().await {
564    ///     let doc = doc?;
565    ///     assert_eq!(doc.id(), "user-2");
566    ///     count += 1;
567    /// }
568    /// assert_eq!(count, 1);
569    /// # Ok(())
570    /// # }
571    /// ```
572    pub fn filter<F>(&self, predicate: F) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
573    where
574        F: Fn(&Document) -> bool + Send + Sync + 'static,
575    {
576        self.filter_with_verification(predicate, &crate::VerificationOptions::default())
577    }
578
579    /// Filters documents in the collection using a predicate function with custom verification
580    /// options.
581    ///
582    /// This method performs streaming filtering by loading and checking documents
583    /// one by one, keeping only matching documents in memory. This approach
584    /// minimizes memory usage while maintaining good performance for most use cases.
585    ///
586    /// # Arguments
587    ///
588    /// * `predicate` - A function that takes a `&Document` and returns `true` if the document
589    ///   should be included in the results.
590    /// * `options` - Verification options controlling hash and signature verification.
591    ///
592    /// # Returns
593    ///
594    /// Returns a stream of documents that match the predicate.
595    ///
596    /// # Example
597    ///
598    /// ```rust
599    /// use sentinel_dbms::{Store, Collection, VerificationOptions};
600    /// use serde_json::json;
601    /// use futures::stream::StreamExt;
602    ///
603    /// # async fn example() -> sentinel_dbms::Result<()> {
604    /// let store = Store::new("/path/to/data", None).await?;
605    /// let collection = store.collection("users").await?;
606    ///
607    /// // Insert some test data
608    /// collection.insert("user-1", json!({"name": "Alice", "age": 25})).await?;
609    /// collection.insert("user-2", json!({"name": "Bob", "age": 30})).await?;
610    ///
611    /// // Filter with warnings enabled
612    /// let options = VerificationOptions::warn();
613    /// let mut adults = collection.filter_with_verification(
614    ///     |doc| {
615    ///         doc.data().get("age")
616    ///             .and_then(|v| v.as_i64())
617    ///             .map_or(false, |age| age > 26)
618    ///     },
619    ///     &options
620    /// );
621    ///
622    /// let mut count = 0;
623    /// while let Some(doc) = adults.next().await {
624    ///     let doc = doc?;
625    ///     assert_eq!(doc.id(), "user-2");
626    ///     count += 1;
627    /// }
628    /// assert_eq!(count, 1);
629    /// # Ok(())
630    /// # }
631    /// ```
632    pub fn filter_with_verification<F>(
633        &self,
634        predicate: F,
635        options: &crate::VerificationOptions,
636    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
637    where
638        F: Fn(&Document) -> bool + Send + Sync + 'static,
639    {
640        let collection_path = self.path.clone();
641        let signing_key = self.signing_key.clone();
642        let options = *options;
643
644        Box::pin(stream! {
645            trace!(
646                "Streaming filter on collection (verification enabled: {})",
647                options.verify_signature || options.verify_hash
648            );
649            let mut entries = match tokio_fs::read_dir(&collection_path).await {
650                Ok(entries) => entries,
651                Err(e) => {
652                    yield Err(e.into());
653                    return;
654                }
655            };
656
657            loop {
658                let entry = match entries.next_entry().await {
659                    Ok(Some(entry)) => entry,
660                    Ok(None) => break,
661                    Err(e) => {
662                        yield Err(e.into());
663                        continue;
664                    }
665                };
666
667                let path = entry.path();
668                if !tokio_fs::metadata(&path).await.map(|m| m.is_dir()).unwrap_or(false)
669                    && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
670                        && file_name.ends_with(".json") && !file_name.starts_with('.') {
671                            let id = file_name.strip_suffix(".json").unwrap();
672                            let file_path = collection_path.join(format!("{}.json", id));
673                            match tokio_fs::read_to_string(&file_path).await {
674                                Ok(content) => {
675                                    match serde_json::from_str::<Document>(&content) {
676                                        Ok(mut doc) => {
677                                            doc.id = id.to_owned();
678
679                                            let collection_ref = Self {
680                                                path: collection_path.clone(),
681                                                signing_key: signing_key.clone(),
682                                            };
683
684                                            if let Err(e) = collection_ref.verify_document(&doc, &options).await {
685                                                if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
686                                                    if options.hash_verification_mode == crate::VerificationMode::Strict
687                                                        || options.signature_verification_mode == crate::VerificationMode::Strict
688                                                    {
689                                                        yield Err(e);
690                                                        continue;
691                                                    }
692                                                } else {
693                                                    yield Err(e);
694                                                    continue;
695                                                }
696                                            }
697
698                                            if predicate(&doc) {
699                                                yield Ok(doc);
700                                            }
701                                        }
702                                        Err(e) => yield Err(e.into()),
703                                    }
704                                }
705                                Err(e) => yield Err(e.into()),
706                            }
707                        }
708            }
709            debug!("Streaming filter completed");
710        })
711    }
712
713    /// Streams all documents in the collection.
714    ///
715    /// This method performs streaming by loading documents one by one,
716    /// minimizing memory usage.
717    ///
718    /// By default, this method verifies both hash and signature with strict mode.
719    /// Use `all_with_verification()` to customize verification behavior.
720    ///
721    /// # Returns
722    ///
723    /// Returns a stream of all documents in the collection.
724    ///
725    /// # Example
726    ///
727    /// ```rust
728    /// use sentinel_dbms::{Collection, Store};
729    /// use futures::stream::StreamExt;
730    ///
731    /// # async fn example() -> sentinel_dbms::Result<()> {
732    /// let store = Store::new("/path/to/data", None).await?;
733    /// let collection = store.collection("users").await?;
734    ///
735    /// // Stream all documents
736    /// let mut all_docs = collection.all();
737    /// while let Some(doc) = all_docs.next().await {
738    ///     let doc = doc?;
739    ///     println!("Document: {}", doc.id());
740    /// }
741    /// # Ok(())
742    /// # }
743    /// ```
744    pub fn all(&self) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>> {
745        self.all_with_verification(&crate::VerificationOptions::default())
746    }
747
748    /// Streams all documents in the collection with custom verification options.
749    ///
750    /// This method performs streaming by loading documents one by one,
751    /// minimizing memory usage.
752    ///
753    /// # Arguments
754    ///
755    /// * `options` - Verification options controlling hash and signature verification.
756    ///
757    /// # Returns
758    ///
759    /// Returns a stream of all documents in the collection.
760    ///
761    /// # Example
762    ///
763    /// ```rust
764    /// use sentinel_dbms::{Collection, Store, VerificationOptions};
765    /// use futures::stream::StreamExt;
766    ///
767    /// # async fn example() -> sentinel_dbms::Result<()> {
768    /// let store = Store::new("/path/to/data", None).await?;
769    /// let collection = store.collection("users").await?;
770    ///
771    /// // Stream all documents with warnings instead of errors
772    /// let options = VerificationOptions::warn();
773    /// let mut all_docs = collection.all_with_verification(&options);
774    /// while let Some(doc) = all_docs.next().await {
775    ///     let doc = doc?;
776    ///     println!("Document: {}", doc.id());
777    /// }
778    /// # Ok(())
779    /// # }
780    /// ```
781    pub fn all_with_verification(
782        &self,
783        options: &crate::VerificationOptions,
784    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>> {
785        let collection_path = self.path.clone();
786        let signing_key = self.signing_key.clone();
787        let options = *options;
788
789        Box::pin(stream! {
790            trace!(
791                "Streaming all documents on collection (verification enabled: {})",
792                options.verify_signature || options.verify_hash
793            );
794            let mut entries = match tokio_fs::read_dir(&collection_path).await {
795                Ok(entries) => entries,
796                Err(e) => {
797                    yield Err(e.into());
798                    return;
799                }
800            };
801
802            loop {
803                let entry = match entries.next_entry().await {
804                    Ok(Some(entry)) => entry,
805                    Ok(None) => break,
806                    Err(e) => {
807                        yield Err(e.into());
808                        continue;
809                    }
810                };
811
812                let path = entry.path();
813                if !tokio_fs::metadata(&path).await.map(|m| m.is_dir()).unwrap_or(false)
814                    && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
815                        && file_name.ends_with(".json") && !file_name.starts_with('.') {
816                            let id = file_name.strip_suffix(".json").unwrap();
817                            let file_path = collection_path.join(format!("{}.json", id));
818                            match tokio_fs::read_to_string(&file_path).await {
819                                Ok(content) => {
820                                    match serde_json::from_str::<Document>(&content) {
821                                        Ok(mut doc) => {
822                                            doc.id = id.to_owned();
823
824                                            let collection_ref = Self {
825                                                path: collection_path.clone(),
826                                                signing_key: signing_key.clone(),
827                                            };
828
829                                            if let Err(e) = collection_ref.verify_document(&doc, &options).await {
830                                                if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
831                                                    if options.hash_verification_mode == crate::VerificationMode::Strict
832                                                        || options.signature_verification_mode == crate::VerificationMode::Strict
833                                                    {
834                                                        yield Err(e);
835                                                        continue;
836                                                    }
837                                                } else {
838                                                    yield Err(e);
839                                                    continue;
840                                                }
841                                            }
842
843                                            yield Ok(doc);
844                                        }
845                                        Err(e) => yield Err(e.into()),
846                                    }
847                                }
848                                Err(e) => yield Err(e.into()),
849                            }
850                        }
851            }
852            debug!("Streaming all completed");
853        })
854    }
855
856    /// Executes a structured query against the collection.
857    ///
858    /// This method supports complex filtering, sorting, pagination, and field projection.
859    /// For optimal performance and memory usage:
860    /// - Queries without sorting use streaming processing with early limit application
861    /// - Queries with sorting collect filtered documents in memory for sorting
862    /// - Projection is applied only to final results to minimize memory usage
863    ///
864    /// By default, this method verifies both hash and signature with strict mode.
865    /// Use `query_with_verification()` to customize verification behavior.
866    ///
867    /// # Arguments
868    ///
869    /// * `query` - The query to execute
870    ///
871    /// # Returns
872    ///
873    /// Returns a `QueryResult` containing the matching documents and metadata.
874    ///
875    /// # Example
876    ///
877    /// ```rust
878    /// use sentinel_dbms::{Store, Collection, QueryBuilder, Operator, SortOrder};
879    /// use serde_json::json;
880    ///
881    /// # async fn example() -> sentinel_dbms::Result<()> {
882    /// let store = Store::new("/path/to/data", None).await?;
883    /// let collection = store.collection("users").await?;
884    ///
885    /// // Insert test data
886    /// collection.insert("user-1", json!({"name": "Alice", "age": 25, "city": "NYC"})).await?;
887    /// collection.insert("user-2", json!({"name": "Bob", "age": 30, "city": "LA"})).await?;
888    /// collection.insert("user-3", json!({"name": "Charlie", "age": 35, "city": "NYC"})).await?;
889    ///
890    /// // Query for users in NYC, sorted by age, limit 2
891    /// let query = QueryBuilder::new()
892    ///     .filter("city", Operator::Equals, json!("NYC"))
893    ///     .sort("age", SortOrder::Ascending)
894    ///     .limit(2)
895    ///     .projection(vec!["name", "age"])
896    ///     .build();
897    ///
898    /// let result = collection.query(query).await?;
899    /// let documents: Vec<_> = futures::TryStreamExt::try_collect(result.documents).await?;
900    /// assert_eq!(documents.len(), 2);
901    /// # Ok(())
902    /// # }
903    /// ```
904    pub async fn query(&self, query: crate::Query) -> Result<crate::QueryResult> {
905        self.query_with_verification(query, &crate::VerificationOptions::default())
906            .await
907    }
908
909    /// Executes a structured query against the collection with custom verification options.
910    ///
911    /// This method supports complex filtering, sorting, pagination, and field projection.
912    /// For optimal performance and memory usage:
913    /// - Queries without sorting use streaming processing with early limit application
914    /// - Queries with sorting collect filtered documents in memory for sorting
915    /// - Projection is applied only to final results to minimize memory usage
916    ///
917    /// # Arguments
918    ///
919    /// * `query` - The query to execute
920    /// * `options` - Verification options controlling hash and signature verification.
921    ///
922    /// # Returns
923    ///
924    /// Returns a `QueryResult` containing the matching documents and metadata.
925    ///
926    /// # Example
927    ///
928    /// ```rust
929    /// use sentinel_dbms::{Store, Collection, QueryBuilder, Operator, SortOrder, VerificationOptions, VerificationMode};
930    /// use serde_json::json;
931    ///
932    /// # async fn example() -> sentinel_dbms::Result<()> {
933    /// let store = Store::new("/path/to/data", None).await?;
934    /// let collection = store.collection("users").await?;
935    ///
936    /// // Insert test data
937    /// collection.insert("user-1", json!({"name": "Alice", "age": 25, "city": "NYC"})).await?;
938    /// collection.insert("user-2", json!({"name": "Bob", "age": 30, "city": "LA"})).await?;
939    /// collection.insert("user-3", json!({"name": "Charlie", "age": 35, "city": "NYC"})).await?;
940    ///
941    /// // Query with warning mode
942    /// let options = VerificationOptions::warn();
943    /// let query = QueryBuilder::new()
944    ///     .filter("city", Operator::Equals, json!("NYC"))
945    ///     .sort("age", SortOrder::Ascending)
946    ///     .limit(2)
947    ///     .projection(vec!["name", "age"])
948    ///     .build();
949    ///
950    /// let result = collection.query_with_verification(query, &options).await?;
951    /// let documents: Vec<_> = futures::TryStreamExt::try_collect(result.documents).await?;
952    /// assert_eq!(documents.len(), 2);
953    /// # Ok(())
954    /// # }
955    /// ```
956    pub async fn query_with_verification(
957        &self,
958        query: crate::Query,
959        options: &crate::VerificationOptions,
960    ) -> Result<crate::QueryResult> {
961        use std::time::Instant;
962        let start_time = Instant::now();
963
964        trace!(
965            "Executing query on collection: {} (verification enabled: {})",
966            self.name(),
967            options.verify_signature || options.verify_hash
968        );
969
970        // Get all document IDs - but for full streaming, we should avoid this
971        // However, for sorted queries, we need to know all IDs to collect
972        // For non-sorted, we can stream without knowing all IDs
973        let documents_stream = if query.sort.is_some() {
974            // For sorted queries, we need to collect all matching documents
975            let all_ids: Vec<String> = self.list().try_collect().await?;
976            let docs = self
977                .execute_sorted_query_with_verification(&all_ids, &query, options)
978                .await?;
979            let stream = tokio_stream::iter(docs.into_iter().map(Ok));
980            Box::pin(stream) as std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
981        }
982        else {
983            // For non-sorted queries, use streaming
984            self.execute_streaming_query_with_verification(&query, options)
985                .await?
986        };
987
988        let execution_time = start_time.elapsed();
989        debug!("Query completed in {:?}", execution_time);
990
991        Ok(crate::QueryResult {
992            documents: documents_stream,
993            total_count: None, // For streaming, we don't know the total count upfront
994            execution_time,
995        })
996    }
997
998    /// Executes a query that requires sorting by collecting all matching documents first with
999    /// verification.
1000    async fn execute_sorted_query_with_verification(
1001        &self,
1002        all_ids: &[String],
1003        query: &crate::Query,
1004        options: &crate::VerificationOptions,
1005    ) -> Result<Vec<Document>> {
1006        // For sorted queries, we need to collect all matching documents to sort them
1007        // But we can optimize by only keeping document IDs and sort values during filtering
1008        let mut matching_docs = Vec::new();
1009
1010        // Precompute filter references to avoid allocating a new Vec for each document
1011        let filter_refs: Vec<_> = query.filters.iter().collect();
1012
1013        for id in all_ids {
1014            if let Some(doc) = self.get_with_verification(id, options).await? &&
1015                matches_filters(&doc, &filter_refs)
1016            {
1017                matching_docs.push(doc);
1018            }
1019        }
1020
1021        if let Some(ref inner) = query.sort {
1022            let field = &inner.0;
1023            let order = &inner.1;
1024            matching_docs.sort_by(|a, b| {
1025                let a_val = a.data().get(field.as_str());
1026                let b_val = b.data().get(field.as_str());
1027                if *order == crate::SortOrder::Ascending {
1028                    self.compare_values(a_val, b_val)
1029                }
1030                else {
1031                    self.compare_values(b_val, a_val)
1032                }
1033            });
1034        }
1035
1036        // Apply offset and limit
1037        let offset = query.offset.unwrap_or(0);
1038        let start_idx = offset.min(matching_docs.len());
1039        let end_idx = query.limit.map_or(matching_docs.len(), |limit| {
1040            start_idx.saturating_add(limit).min(matching_docs.len())
1041        });
1042
1043        // Apply projection to the final results
1044        let mut final_docs = Vec::new();
1045        for doc in matching_docs
1046            .into_iter()
1047            .skip(start_idx)
1048            .take(end_idx.saturating_sub(start_idx))
1049        {
1050            let projected_doc = if let Some(ref fields) = query.projection {
1051                self.project_document(&doc, fields).await?
1052            }
1053            else {
1054                doc
1055            };
1056            final_docs.push(projected_doc);
1057        }
1058
1059        Ok(final_docs)
1060    }
1061
1062    /// Executes a query without sorting, allowing streaming with early limit application and
1063    /// verification.
1064    async fn execute_streaming_query_with_verification(
1065        &self,
1066        query: &crate::Query,
1067        options: &crate::VerificationOptions,
1068    ) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>> {
1069        let collection_path = self.path.clone();
1070        let signing_key = self.signing_key.clone();
1071        let filters = query.filters.clone();
1072        let projection_fields = query.projection.clone();
1073        let limit = query.limit.unwrap_or(usize::MAX);
1074        let offset = query.offset.unwrap_or(0);
1075        let options = *options;
1076
1077        Ok(Box::pin(stream! {
1078            let mut id_stream = stream_document_ids(collection_path.clone());
1079            let mut yielded = 0;
1080            let mut skipped = 0;
1081
1082            // Precompute filter references to avoid allocating a new Vec for each document
1083            let filter_refs: Vec<_> = filters.iter().collect();
1084
1085            while let Some(id_result) = id_stream.next().await {
1086                let id = match id_result {
1087                    Ok(id) => id,
1088                    Err(e) => {
1089                        yield Err(e);
1090                        continue;
1091                    }
1092                };
1093
1094                // Load document
1095                let file_path = collection_path.join(format!("{}.json", id));
1096                let content = match tokio_fs::read_to_string(&file_path).await {
1097                    Ok(content) => content,
1098                    Err(e) => {
1099                        yield Err(e.into());
1100                        continue;
1101                    }
1102                };
1103
1104                let doc = match serde_json::from_str::<Document>(&content) {
1105                    Ok(doc) => {
1106                        // Create a new document with the correct ID
1107                        let mut doc_with_id = doc;
1108                        doc_with_id.id = id.clone();
1109
1110                        let collection_ref = Self {
1111                            path: collection_path.clone(),
1112                            signing_key: signing_key.clone(),
1113                        };
1114
1115                        if let Err(e) = collection_ref.verify_document(&doc_with_id, &options).await {
1116                            if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
1117                                if options.hash_verification_mode == crate::VerificationMode::Strict
1118                                    || options.signature_verification_mode == crate::VerificationMode::Strict
1119                                {
1120                                    yield Err(e);
1121                                    continue;
1122                                }
1123                            } else {
1124                                yield Err(e);
1125                                continue;
1126                            }
1127                        }
1128
1129                        doc_with_id
1130                    }
1131                    Err(e) => {
1132                        yield Err(e.into());
1133                        continue;
1134                    }
1135                };
1136
1137                if matches_filters(&doc, &filter_refs) {
1138                    if skipped < offset {
1139                        skipped = skipped.saturating_add(1);
1140                        continue;
1141                    }
1142                    if yielded >= limit {
1143                        break;
1144                    }
1145                    let final_doc = if let Some(ref fields) = projection_fields {
1146                        project_document(&doc, fields).await?
1147                    } else {
1148                        doc
1149                    };
1150                    yield Ok(final_doc);
1151                    yielded = yielded.saturating_add(1);
1152                }
1153            }
1154        }))
1155    }
1156
1157    /// Compares two values for sorting purposes.
1158    fn compare_values(&self, a: Option<&Value>, b: Option<&Value>) -> std::cmp::Ordering { compare_values(a, b) }
1159
1160    /// Projects a document to include only specified fields.
1161    async fn project_document(&self, doc: &Document, fields: &[String]) -> Result<Document> {
1162        project_document(doc, fields).await
1163    }
1164
1165    /// Verifies document hash according to the specified verification options.
1166    ///
1167    /// # Arguments
1168    ///
1169    /// * `doc` - The document to verify
1170    /// * `options` - The verification options
1171    ///
1172    /// # Returns
1173    ///
1174    /// Returns `Ok(())` if verification passes or is handled according to the mode,
1175    /// or `Err(SentinelError::HashVerificationFailed)` if verification fails in Strict mode.
1176    async fn verify_hash(&self, doc: &Document, options: crate::VerificationOptions) -> Result<()> {
1177        if options.hash_verification_mode == crate::VerificationMode::Silent {
1178            return Ok(());
1179        }
1180
1181        trace!("Verifying hash for document: {}", doc.id());
1182        let computed_hash = sentinel_crypto::hash_data(doc.data()).await?;
1183
1184        if computed_hash != doc.hash() {
1185            let reason = format!(
1186                "Expected hash: {}, Computed hash: {}",
1187                doc.hash(),
1188                computed_hash
1189            );
1190
1191            match options.hash_verification_mode {
1192                crate::VerificationMode::Strict => {
1193                    error!("Document {} hash verification failed: {}", doc.id(), reason);
1194                    return Err(SentinelError::HashVerificationFailed {
1195                        id: doc.id().to_owned(),
1196                        reason,
1197                    });
1198                },
1199                crate::VerificationMode::Warn => {
1200                    warn!("Document {} hash verification failed: {}", doc.id(), reason);
1201                },
1202                crate::VerificationMode::Silent => {},
1203            }
1204        }
1205        else {
1206            trace!("Document {} hash verified successfully", doc.id());
1207        }
1208
1209        Ok(())
1210    }
1211
1212    /// Verifies document signature according to the specified verification options.
1213    ///
1214    /// # Arguments
1215    ///
1216    /// * `doc` - The document to verify
1217    /// * `options` - The verification options containing modes for different scenarios
1218    ///
1219    /// # Returns
1220    ///
1221    /// Returns `Ok(())` if verification passes or is handled according to the mode,
1222    /// or `Err(SentinelError::SignatureVerificationFailed)` if verification fails in Strict mode.
1223    async fn verify_signature(&self, doc: &Document, options: crate::VerificationOptions) -> Result<()> {
1224        if options.signature_verification_mode == crate::VerificationMode::Silent &&
1225            options.empty_signature_mode == crate::VerificationMode::Silent
1226        {
1227            return Ok(());
1228        }
1229
1230        trace!("Verifying signature for document: {}", doc.id());
1231
1232        if doc.signature().is_empty() {
1233            let reason = "Document has no signature".to_owned();
1234
1235            match options.empty_signature_mode {
1236                crate::VerificationMode::Strict => {
1237                    error!("Document {} has no signature: {}", doc.id(), reason);
1238                    return Err(SentinelError::SignatureVerificationFailed {
1239                        id: doc.id().to_owned(),
1240                        reason,
1241                    });
1242                },
1243                crate::VerificationMode::Warn => {
1244                    warn!("Document {} has no signature: {}", doc.id(), reason);
1245                },
1246                crate::VerificationMode::Silent => {},
1247            }
1248            return Ok(());
1249        }
1250
1251        if !options.verify_signature {
1252            trace!("Signature verification disabled for document: {}", doc.id());
1253            return Ok(());
1254        }
1255
1256        if let Some(ref signing_key) = self.signing_key {
1257            let public_key = signing_key.verifying_key();
1258            let is_valid = sentinel_crypto::verify_signature(doc.hash(), doc.signature(), &public_key).await?;
1259
1260            if !is_valid {
1261                let reason = "Signature verification using public key failed".to_owned();
1262
1263                match options.signature_verification_mode {
1264                    crate::VerificationMode::Strict => {
1265                        error!(
1266                            "Document {} signature verification failed: {}",
1267                            doc.id(),
1268                            reason
1269                        );
1270                        return Err(SentinelError::SignatureVerificationFailed {
1271                            id: doc.id().to_owned(),
1272                            reason,
1273                        });
1274                    },
1275                    crate::VerificationMode::Warn => {
1276                        warn!(
1277                            "Document {} signature verification failed: {}",
1278                            doc.id(),
1279                            reason
1280                        );
1281                    },
1282                    crate::VerificationMode::Silent => {},
1283                }
1284            }
1285            else {
1286                trace!("Document {} signature verified successfully", doc.id());
1287            }
1288        }
1289        else {
1290            trace!("No signing key available for verification, skipping signature check");
1291        }
1292
1293        Ok(())
1294    }
1295
1296    /// Verifies both hash and signature of a document according to the specified options.
1297    ///
1298    /// # Arguments
1299    ///
1300    /// * `doc` - The document to verify
1301    /// * `options` - The verification options
1302    ///
1303    /// # Returns
1304    ///
1305    /// Returns `Ok(())` if verifications pass or are handled according to the modes,
1306    /// or an error if verification fails in Strict mode.
1307    async fn verify_document(&self, doc: &Document, options: &crate::VerificationOptions) -> Result<()> {
1308        if options.verify_hash {
1309            self.verify_hash(doc, *options).await?;
1310        }
1311
1312        if options.verify_signature {
1313            self.verify_signature(doc, *options).await?;
1314        }
1315
1316        Ok(())
1317    }
1318
1319    /// Updates a document by merging new data with existing data.
1320    ///
1321    /// This method loads the existing document, merges the provided data with the existing
1322    /// document data (deep merge for objects), updates the metadata (updated_at timestamp),
1323    /// and saves the document back to disk.
1324    ///
1325    /// If the document doesn't exist, this method will return an error.
1326    ///
1327    /// # Arguments
1328    ///
1329    /// * `id` - The unique identifier of the document to update
1330    /// * `data` - The new data to merge with the existing document data
1331    ///
1332    /// # Returns
1333    ///
1334    /// Returns `Ok(())` on success, or a `SentinelError` if the operation fails.
1335    ///
1336    /// # Examples
1337    ///
1338    /// ```rust
1339    /// use sentinel_dbms::{Store, Collection};
1340    /// use serde_json::json;
1341    ///
1342    /// # async fn example() -> sentinel_dbms::Result<()> {
1343    /// let store = Store::new("/path/to/data", None).await?;
1344    /// let collection = store.collection("users").await?;
1345    ///
1346    /// // Insert initial document
1347    /// collection.insert("user-123", json!({"name": "Alice", "age": 30})).await?;
1348    ///
1349    /// // Update with partial data (only age)
1350    /// collection.update("user-123", json!({"age": 31, "city": "NYC"})).await?;
1351    ///
1352    /// // Document now contains: {"name": "Alice", "age": 31, "city": "NYC"}
1353    /// let doc = collection.get("user-123").await?.unwrap();
1354    /// assert_eq!(doc.data()["name"], "Alice");
1355    /// assert_eq!(doc.data()["age"], 31);
1356    /// assert_eq!(doc.data()["city"], "NYC");
1357    /// # Ok(())
1358    /// # }
1359    /// ```
1360    /// Merges two JSON values, with `new_value` taking precedence over `existing_value`.
1361    ///
1362    /// For objects, this performs a deep merge where fields from `new_value` override
1363    /// or add to fields in `existing_value`. For other types, `new_value` completely replaces
1364    /// `existing_value`.
1365    #[allow(
1366        clippy::pattern_type_mismatch,
1367        reason = "false positive with serde_json::Value"
1368    )]
1369    fn merge_json_values(existing_value: &Value, new_value: Value) -> Value {
1370        match (existing_value, &new_value) {
1371            (Value::Object(existing_map), Value::Object(new_map)) => {
1372                let mut merged = existing_map.clone();
1373                for (key, value) in new_map {
1374                    merged.insert(key.clone(), value.clone());
1375                }
1376                Value::Object(merged)
1377            },
1378            _ => new_value,
1379        }
1380    }
1381
1382    /// Extracts a numeric value from a document field for aggregation operations.
1383    fn extract_numeric_value(doc: &Document, field: &str) -> Option<f64> {
1384        doc.data().get(field).and_then(|v| {
1385            match *v {
1386                Value::Number(ref n) => n.as_f64(),
1387                Value::Null | Value::Bool(_) | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
1388            }
1389        })
1390    }
1391
1392    pub async fn update(&self, id: &str, data: Value) -> Result<()> {
1393        trace!("Updating document with id: {}", id);
1394        validate_document_id(id)?;
1395
1396        // Load existing document
1397        let Some(mut existing_doc) = self.get(id).await?
1398        else {
1399            return Err(SentinelError::DocumentNotFound {
1400                id:         id.to_owned(),
1401                collection: self.name().to_owned(),
1402            });
1403        };
1404
1405        // Merge the new data with existing data
1406        let merged_data = Self::merge_json_values(existing_doc.data(), data);
1407
1408        // Update the document data and metadata
1409        if let Some(key) = self.signing_key.as_ref() {
1410            existing_doc.set_data(merged_data, key).await?;
1411        }
1412        else {
1413            // For unsigned documents, we need to manually update the data and hash
1414            existing_doc.data = merged_data;
1415            existing_doc.updated_at = chrono::Utc::now();
1416            existing_doc.hash = sentinel_crypto::hash_data(&existing_doc.data).await?;
1417            existing_doc.signature = String::new();
1418        }
1419
1420        // Save the updated document
1421        let file_path = self.path.join(format!("{}.json", id));
1422        let json = serde_json::to_string_pretty(&existing_doc).map_err(|e| {
1423            error!("Failed to serialize updated document {} to JSON: {}", id, e);
1424            e
1425        })?;
1426        tokio_fs::write(&file_path, json).await.map_err(|e| {
1427            error!(
1428                "Failed to write updated document {} to file {:?}: {}",
1429                id, file_path, e
1430            );
1431            e
1432        })?;
1433
1434        debug!("Document {} updated successfully", id);
1435        Ok(())
1436    }
1437
1438    /// Retrieves multiple documents by their IDs in a single operation.
1439    ///
1440    /// This method efficiently loads multiple documents concurrently. For IDs that don't exist,
1441    /// `None` is returned in the corresponding position.
1442    ///
1443    /// # Arguments
1444    ///
1445    /// * `ids` - A slice of document IDs to retrieve
1446    ///
1447    /// # Returns
1448    ///
1449    /// Returns a `Vec<Option<Document>>` where each element corresponds to the document
1450    /// at the same index in the input `ids` slice. `Some(document)` if the document exists,
1451    /// `None` if it doesn't exist.
1452    ///
1453    /// # Examples
1454    ///
1455    /// ```rust
1456    /// use sentinel_dbms::{Store, Collection};
1457    /// use serde_json::json;
1458    ///
1459    /// # async fn example() -> sentinel_dbms::Result<()> {
1460    /// let store = Store::new("/path/to/data", None).await?;
1461    /// let collection = store.collection("users").await?;
1462    ///
1463    /// // Insert some documents
1464    /// collection.insert("user-1", json!({"name": "Alice"})).await?;
1465    /// collection.insert("user-2", json!({"name": "Bob"})).await?;
1466    ///
1467    /// // Batch get multiple documents
1468    /// let docs = collection.get_many(&["user-1", "user-2", "user-3"]).await?;
1469    /// assert_eq!(docs.len(), 3);
1470    /// assert!(docs[0].is_some()); // user-1 exists
1471    /// assert!(docs[1].is_some()); // user-2 exists
1472    /// assert!(docs[2].is_none()); // user-3 doesn't exist
1473    /// # Ok(())
1474    /// # }
1475    /// ```
1476    pub async fn get_many(&self, ids: &[&str]) -> Result<Vec<Option<Document>>> {
1477        use futures::future::join_all;
1478
1479        trace!("Batch getting {} documents", ids.len());
1480
1481        let futures = ids.iter().map(|&id| self.get(id));
1482        let results = join_all(futures).await;
1483
1484        let documents = results.into_iter().collect::<Result<Vec<_>>>()?;
1485
1486        debug!(
1487            "Batch get completed, retrieved {} documents",
1488            documents.len()
1489        );
1490        Ok(documents)
1491    }
1492
1493    /// Inserts a document if it doesn't exist, or updates it if it does.
1494    ///
1495    /// This is a convenience method that combines insert and update operations.
1496    /// If the document doesn't exist, it will be inserted. If it exists, the new data
1497    /// will be merged with the existing data (see `update` for merge behavior).
1498    ///
1499    /// # Arguments
1500    ///
1501    /// * `id` - The unique identifier of the document
1502    /// * `data` - The data to insert or merge
1503    ///
1504    /// # Returns
1505    ///
1506    /// Returns `Ok(true)` if a new document was inserted, `Ok(false)` if an existing
1507    /// document was updated.
1508    ///
1509    /// # Examples
1510    ///
1511    /// ```rust
1512    /// use sentinel_dbms::{Store, Collection};
1513    /// use serde_json::json;
1514    ///
1515    /// # async fn example() -> sentinel_dbms::Result<()> {
1516    /// let store = Store::new("/path/to/data", None).await?;
1517    /// let collection = store.collection("users").await?;
1518    ///
1519    /// // First call inserts the document
1520    /// let inserted = collection.upsert("user-123", json!({"name": "Alice"})).await?;
1521    /// assert!(inserted);
1522    ///
1523    /// // Second call updates the existing document
1524    /// let updated = collection.upsert("user-123", json!({"age": 30})).await?;
1525    /// assert!(!updated);
1526    ///
1527    /// // Document now contains both name and age
1528    /// let doc = collection.get("user-123").await?.unwrap();
1529    /// assert_eq!(doc.data()["name"], "Alice");
1530    /// assert_eq!(doc.data()["age"], 30);
1531    /// # Ok(())
1532    /// # }
1533    /// ```
1534    pub async fn upsert(&self, id: &str, data: Value) -> Result<bool> {
1535        trace!("Upserting document with id: {}", id);
1536
1537        if self.get(id).await?.is_some() {
1538            // Document exists, update it
1539            self.update(id, data).await?;
1540            debug!("Document {} updated via upsert", id);
1541            Ok(false)
1542        }
1543        else {
1544            // Document doesn't exist, insert it
1545            self.insert(id, data).await?;
1546            debug!("Document {} inserted via upsert", id);
1547            Ok(true)
1548        }
1549    }
1550
1551    /// Performs aggregation operations on documents matching the given filters.
1552    ///
1553    /// Supported aggregations:
1554    /// - `Count`: Count of matching documents
1555    /// - `Sum(field)`: Sum of numeric values in the specified field
1556    /// - `Avg(field)`: Average of numeric values in the specified field
1557    /// - `Min(field)`: Minimum value in the specified field
1558    /// - `Max(field)`: Maximum value in the specified field
1559    ///
1560    /// # Arguments
1561    ///
1562    /// * `filters` - Filters to apply before aggregation
1563    /// * `aggregation` - The aggregation operation to perform
1564    ///
1565    /// # Returns
1566    ///
1567    /// Returns the aggregated result as a JSON `Value`.
1568    ///
1569    /// # Examples
1570    ///
1571    /// ```rust
1572    /// use sentinel_dbms::{Store, Collection, Filter, Aggregation};
1573    /// use serde_json::json;
1574    ///
1575    /// # async fn example() -> sentinel_dbms::Result<()> {
1576    /// let store = Store::new("/path/to/data", None).await?;
1577    /// let collection = store.collection("products").await?;
1578    ///
1579    /// // Insert some test data
1580    /// collection.insert("prod-1", json!({"name": "Widget", "price": 10.0})).await?;
1581    /// collection.insert("prod-2", json!({"name": "Gadget", "price": 20.0})).await?;
1582    ///
1583    /// // Count all products
1584    /// let count = collection.aggregate(vec![], Aggregation::Count).await?;
1585    /// assert_eq!(count, json!(2));
1586    ///
1587    /// // Sum of all prices
1588    /// let total = collection.aggregate(vec![], Aggregation::Sum("price".to_string())).await?;
1589    /// assert_eq!(total, json!(30.0));
1590    /// # Ok(())
1591    /// # }
1592    /// ```
1593    pub async fn aggregate(&self, filters: Vec<crate::Filter>, aggregation: Aggregation) -> Result<Value> {
1594        use futures::TryStreamExt as _;
1595
1596        trace!("Performing aggregation: {:?}", aggregation);
1597
1598        // Get all documents (we'll filter them)
1599        let mut stream = self.all();
1600
1601        let mut count = 0usize;
1602        let mut sum = 0.0f64;
1603        let mut min = f64::INFINITY;
1604        let mut max = f64::NEG_INFINITY;
1605
1606        while let Some(doc) = stream.try_next().await? {
1607            // Apply filters
1608            if !filters.is_empty() {
1609                let filter_refs: Vec<&Filter> = filters.iter().collect();
1610                if !crate::filtering::matches_filters(&doc, &filter_refs) {
1611                    continue;
1612                }
1613            }
1614
1615            count = count.saturating_add(1);
1616
1617            // Extract value for field-based aggregations
1618            if let Aggregation::Sum(ref field) |
1619            Aggregation::Avg(ref field) |
1620            Aggregation::Min(ref field) |
1621            Aggregation::Max(ref field) = aggregation &&
1622                let Some(value) = Self::extract_numeric_value(&doc, field)
1623            {
1624                sum += value;
1625                min = min.min(value);
1626                max = max.max(value);
1627            }
1628        }
1629
1630        let result = match aggregation {
1631            Aggregation::Count => json!(count),
1632            Aggregation::Sum(_) => json!(sum),
1633            Aggregation::Avg(_) => {
1634                if count == 0 {
1635                    json!(null)
1636                }
1637                else {
1638                    json!(sum / count as f64)
1639                }
1640            },
1641            Aggregation::Min(_) => {
1642                if min == f64::INFINITY {
1643                    json!(null)
1644                }
1645                else {
1646                    json!(min)
1647                }
1648            },
1649            Aggregation::Max(_) => {
1650                if max == f64::NEG_INFINITY {
1651                    json!(null)
1652                }
1653                else {
1654                    json!(max)
1655                }
1656            },
1657        };
1658
1659        debug!("Aggregation result: {}", result);
1660        Ok(result)
1661    }
1662}
1663
1664/// Validates that a document ID is filesystem-safe across all platforms.
1665///
1666/// # Rules
1667/// - Must not be empty
1668/// - Must not contain path separators (`/` or `\`)
1669/// - Must not contain control characters (0x00-0x1F, 0x7F)
1670/// - Must not be a Windows reserved name (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
1671/// - Must not contain Windows reserved characters (< > : " | ? *)
1672/// - Must only contain valid filename characters
1673///
1674/// # Parameters
1675/// - `id`: The document ID to validate
1676///
1677/// # Returns
1678/// - `Ok(())` if the ID is valid
1679/// - `Err(SentinelError::InvalidDocumentId)` if the ID is invalid
1680pub fn validate_document_id(id: &str) -> Result<()> {
1681    trace!("Validating document id: {}", id);
1682    // Check if id is empty
1683    if id.is_empty() {
1684        warn!("Document id is empty");
1685        return Err(SentinelError::InvalidDocumentId {
1686            id: id.to_owned(),
1687        });
1688    }
1689
1690    // Check for valid characters
1691    if !is_valid_document_id_chars(id) {
1692        warn!("Document id contains invalid characters: {}", id);
1693        return Err(SentinelError::InvalidDocumentId {
1694            id: id.to_owned(),
1695        });
1696    }
1697
1698    // Check for Windows reserved names
1699    if is_reserved_name(id) {
1700        warn!("Document id is a reserved name: {}", id);
1701        return Err(SentinelError::InvalidDocumentId {
1702            id: id.to_owned(),
1703        });
1704    }
1705
1706    trace!("Document id '{}' is valid", id);
1707    Ok(())
1708}
1709
1710#[cfg(test)]
1711mod tests {
1712    use serde_json::json;
1713    use tempfile::tempdir;
1714    use tracing_subscriber;
1715
1716    use super::*;
1717    use crate::Store;
1718
1719    /// Helper function to set up a temporary collection for testing
1720    async fn setup_collection() -> (Collection, tempfile::TempDir) {
1721        // Initialize tracing for tests to ensure debug! macros are executed
1722        let _ = tracing_subscriber::fmt()
1723            .with_env_filter("debug")
1724            .try_init();
1725
1726        let temp_dir = tempdir().unwrap();
1727        let store = Store::new(temp_dir.path(), None).await.unwrap();
1728        let collection = store.collection("test_collection").await.unwrap();
1729        (collection, temp_dir)
1730    }
1731
1732    /// Helper function to set up a temporary collection with signing key for testing
1733    async fn setup_collection_with_signing_key() -> (Collection, tempfile::TempDir) {
1734        // Initialize tracing for tests to ensure debug! macros are executed
1735        let _ = tracing_subscriber::fmt()
1736            .with_env_filter("debug")
1737            .try_init();
1738
1739        let temp_dir = tempdir().unwrap();
1740        let store = Store::new(temp_dir.path(), Some("test_passphrase"))
1741            .await
1742            .unwrap();
1743        let collection = store.collection("test_collection").await.unwrap();
1744        (collection, temp_dir)
1745    }
1746
1747    #[tokio::test]
1748    async fn test_insert_and_retrieve() {
1749        let (collection, _temp_dir) = setup_collection().await;
1750
1751        let doc = json!({ "name": "Alice", "email": "alice@example.com" });
1752        collection.insert("user-123", doc.clone()).await.unwrap();
1753
1754        let retrieved = collection
1755            .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1756            .await
1757            .unwrap();
1758        assert_eq!(*retrieved.unwrap().data(), doc);
1759    }
1760
1761    #[tokio::test]
1762    async fn test_insert_empty_document() {
1763        let (collection, _temp_dir) = setup_collection().await;
1764
1765        let doc = json!({});
1766        collection.insert("empty", doc.clone()).await.unwrap();
1767
1768        let retrieved = collection
1769            .get_with_verification("empty", &crate::VerificationOptions::disabled())
1770            .await
1771            .unwrap();
1772        assert_eq!(*retrieved.unwrap().data(), doc);
1773    }
1774
1775    #[tokio::test]
1776    async fn test_insert_with_signing_key() {
1777        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
1778
1779        let doc = json!({ "name": "Alice", "signed": true });
1780        collection.insert("signed_doc", doc.clone()).await.unwrap();
1781
1782        let retrieved = collection.get("signed_doc").await.unwrap().unwrap();
1783        assert_eq!(*retrieved.data(), doc);
1784        // Check that signature is not empty
1785        assert!(!retrieved.signature().is_empty());
1786    }
1787
1788    #[tokio::test]
1789    async fn test_insert_large_document() {
1790        let (collection, _temp_dir) = setup_collection().await;
1791
1792        let large_data = json!({
1793            "large_array": (0..1000).collect::<Vec<_>>(),
1794            "nested": {
1795                "deep": {
1796                    "value": "test"
1797                }
1798            }
1799        });
1800        collection
1801            .insert("large", large_data.clone())
1802            .await
1803            .unwrap();
1804
1805        let retrieved = collection
1806            .get_with_verification("large", &crate::VerificationOptions::disabled())
1807            .await
1808            .unwrap();
1809        assert_eq!(*retrieved.unwrap().data(), large_data);
1810    }
1811
1812    #[tokio::test]
1813    async fn test_insert_with_invalid_special_characters_in_id() {
1814        let (collection, _temp_dir) = setup_collection().await;
1815
1816        let doc = json!({ "data": "test" });
1817        let result = collection.insert("user_123-special!", doc.clone()).await;
1818
1819        // Should return an error for invalid document ID with special characters
1820        assert!(result.is_err());
1821        match result {
1822            Err(SentinelError::InvalidDocumentId {
1823                id,
1824            }) => {
1825                assert_eq!(id, "user_123-special!");
1826            },
1827            _ => panic!("Expected InvalidDocumentId error"),
1828        }
1829    }
1830
1831    #[tokio::test]
1832    async fn test_insert_with_valid_document_ids() {
1833        let (collection, _temp_dir) = setup_collection().await;
1834
1835        // Test various valid document IDs
1836        let valid_ids = vec![
1837            "user-123",
1838            "user_456",
1839            "user123",
1840            "123",
1841            "a",
1842            "user-123_test",
1843            "user_123-test",
1844            "CamelCaseID",
1845            "lower_case_id",
1846            "UPPER_CASE_ID",
1847        ];
1848
1849        for id in valid_ids {
1850            let doc = json!({ "data": "test" });
1851            let result = collection.insert(id, doc).await;
1852            assert!(
1853                result.is_ok(),
1854                "Expected ID '{}' to be valid but got error: {:?}",
1855                id,
1856                result
1857            );
1858        }
1859    }
1860
1861    #[tokio::test]
1862    async fn test_insert_with_various_invalid_document_ids() {
1863        let (collection, _temp_dir) = setup_collection().await;
1864
1865        // Test various invalid document IDs
1866        let invalid_ids = vec![
1867            "user!123",    // exclamation mark
1868            "user@domain", // at sign
1869            "user#123",    // hash
1870            "user$123",    // dollar sign
1871            "user%123",    // percent
1872            "user^123",    // caret
1873            "user&123",    // ampersand
1874            "user*123",    // asterisk
1875            "user(123)",   // parentheses
1876            "user.123",    // period
1877            "user/123",    // forward slash
1878            "user\\123",   // backslash
1879            "user:123",    // colon
1880            "user;123",    // semicolon
1881            "user<123",    // less than
1882            "user>123",    // greater than
1883            "user?123",    // question mark
1884            "user|123",    // pipe
1885            "user\"123",   // quote
1886            "user'123",    // single quote
1887            "",            // empty string
1888        ];
1889
1890        for id in invalid_ids {
1891            let doc = json!({ "data": "test" });
1892            let result = collection.insert(id, doc).await;
1893            assert!(
1894                result.is_err(),
1895                "Expected ID '{}' to be invalid but insertion succeeded",
1896                id
1897            );
1898            match result {
1899                Err(SentinelError::InvalidDocumentId {
1900                    ..
1901                }) => {
1902                    // Expected error type
1903                },
1904                _ => panic!("Expected InvalidDocumentId error for ID '{}'", id),
1905            }
1906        }
1907    }
1908
1909    #[tokio::test]
1910    async fn test_get_nonexistent() {
1911        let (collection, _temp_dir) = setup_collection().await;
1912
1913        let retrieved = collection.get("nonexistent").await.unwrap();
1914        assert!(retrieved.is_none());
1915    }
1916
1917    #[tokio::test]
1918    async fn test_update() {
1919        let (collection, _temp_dir) = setup_collection().await;
1920
1921        let doc1 = json!({ "name": "Alice" });
1922        collection.insert("user-123", doc1).await.unwrap();
1923
1924        let doc2 = json!({ "name": "Alice", "age": 30 });
1925        collection.update("user-123", doc2.clone()).await.unwrap();
1926
1927        let retrieved = collection
1928            .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1929            .await
1930            .unwrap();
1931        assert_eq!(*retrieved.unwrap().data(), doc2);
1932    }
1933
1934    #[tokio::test]
1935    async fn test_update_nonexistent() {
1936        let (collection, _temp_dir) = setup_collection().await;
1937
1938        let doc = json!({ "name": "Bob" });
1939        let result = collection.update("new-user", doc.clone()).await;
1940
1941        // Should return an error for non-existent document
1942        assert!(result.is_err());
1943        match result.unwrap_err() {
1944            crate::SentinelError::DocumentNotFound {
1945                id,
1946                collection: _,
1947            } => {
1948                assert_eq!(id, "new-user");
1949            },
1950            _ => panic!("Expected DocumentNotFound error"),
1951        }
1952    }
1953
1954    #[tokio::test]
1955    async fn test_update_with_invalid_id() {
1956        let (collection, _temp_dir) = setup_collection().await;
1957
1958        let doc = json!({ "name": "Bob" });
1959        let result = collection.update("user!invalid", doc).await;
1960
1961        // Should return an error for invalid document ID
1962        assert!(result.is_err());
1963        match result {
1964            Err(SentinelError::InvalidDocumentId {
1965                id,
1966            }) => {
1967                assert_eq!(id, "user!invalid");
1968            },
1969            _ => panic!("Expected InvalidDocumentId error"),
1970        }
1971    }
1972
1973    #[tokio::test]
1974    async fn test_delete() {
1975        let (collection, _temp_dir) = setup_collection().await;
1976
1977        let doc = json!({ "name": "Alice" });
1978        collection.insert("user-123", doc).await.unwrap();
1979
1980        let retrieved = collection
1981            .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1982            .await
1983            .unwrap();
1984        assert!(retrieved.is_some());
1985
1986        collection.delete("user-123").await.unwrap();
1987
1988        let retrieved = collection
1989            .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1990            .await
1991            .unwrap();
1992        assert!(retrieved.is_none());
1993
1994        // Check that file was moved to .deleted/
1995        let deleted_path = collection.path.join(".deleted").join("user-123.json");
1996        assert!(tokio_fs::try_exists(&deleted_path).await.unwrap());
1997    }
1998
1999    #[tokio::test]
2000    async fn test_delete_nonexistent() {
2001        let (collection, _temp_dir) = setup_collection().await;
2002
2003        // Should not error
2004        collection.delete("nonexistent").await.unwrap();
2005    }
2006
2007    #[tokio::test]
2008    async fn test_list_empty_collection() {
2009        let (collection, _temp_dir) = setup_collection().await;
2010
2011        let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2012        assert!(ids.is_empty());
2013    }
2014
2015    #[tokio::test]
2016    async fn test_list_with_documents() {
2017        let (collection, _temp_dir) = setup_collection().await;
2018
2019        collection
2020            .insert("user-123", json!({"name": "Alice"}))
2021            .await
2022            .unwrap();
2023        collection
2024            .insert("user-456", json!({"name": "Bob"}))
2025            .await
2026            .unwrap();
2027        collection
2028            .insert("user-789", json!({"name": "Charlie"}))
2029            .await
2030            .unwrap();
2031
2032        let mut ids: Vec<String> = collection.list().try_collect().await.unwrap();
2033        ids.sort(); // Sort for consistent ordering in test
2034        assert_eq!(ids.len(), 3);
2035        assert_eq!(ids, vec!["user-123", "user-456", "user-789"]);
2036    }
2037
2038    #[tokio::test]
2039    async fn test_list_skips_deleted_documents() {
2040        let (collection, _temp_dir) = setup_collection().await;
2041
2042        collection
2043            .insert("user-123", json!({"name": "Alice"}))
2044            .await
2045            .unwrap();
2046        collection
2047            .insert("user-456", json!({"name": "Bob"}))
2048            .await
2049            .unwrap();
2050        collection.delete("user-456").await.unwrap();
2051
2052        let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2053        assert_eq!(ids.len(), 1);
2054        assert_eq!(ids, vec!["user-123"]);
2055    }
2056
2057    #[tokio::test]
2058    async fn test_bulk_insert() {
2059        let (collection, _temp_dir) = setup_collection().await;
2060
2061        let documents = vec![
2062            ("user-123", json!({"name": "Alice", "role": "admin"})),
2063            ("user-456", json!({"name": "Bob", "role": "user"})),
2064            ("user-789", json!({"name": "Charlie", "role": "user"})),
2065        ];
2066
2067        collection.bulk_insert(documents).await.unwrap();
2068
2069        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2070        assert_eq!(docs.len(), 3);
2071
2072        let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2073        assert_eq!(ids.len(), 3);
2074        assert!(ids.contains(&"user-123".to_string()));
2075        assert!(ids.contains(&"user-456".to_string()));
2076        assert!(ids.contains(&"user-789".to_string()));
2077
2078        // Verify data
2079        let alice = collection
2080            .get_with_verification("user-123", &crate::VerificationOptions::disabled())
2081            .await
2082            .unwrap()
2083            .unwrap();
2084        assert_eq!(alice.data()["name"], "Alice");
2085        assert_eq!(alice.data()["role"], "admin");
2086    }
2087
2088    #[tokio::test]
2089    async fn test_bulk_insert_empty() {
2090        let (collection, _temp_dir) = setup_collection().await;
2091
2092        collection.bulk_insert(vec![]).await.unwrap();
2093
2094        let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2095        assert!(ids.is_empty());
2096    }
2097
2098    #[tokio::test]
2099    async fn test_bulk_insert_with_invalid_id() {
2100        let (collection, _temp_dir) = setup_collection().await;
2101
2102        let documents = vec![
2103            ("user-123", json!({"name": "Alice"})),
2104            ("user!invalid", json!({"name": "Bob"})),
2105        ];
2106
2107        let result = collection.bulk_insert(documents).await;
2108        assert!(result.is_err());
2109
2110        // First document should have been inserted before error
2111        let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2112        assert_eq!(ids.len(), 1);
2113        assert_eq!(ids[0], "user-123");
2114    }
2115
2116    #[tokio::test]
2117    async fn test_multiple_operations() {
2118        let (collection, _temp_dir) = setup_collection().await;
2119
2120        // Insert multiple
2121        collection
2122            .insert("user1", json!({"name": "User1"}))
2123            .await
2124            .unwrap();
2125        collection
2126            .insert("user2", json!({"name": "User2"}))
2127            .await
2128            .unwrap();
2129
2130        // Get both
2131        let user1 = collection
2132            .get_with_verification("user1", &crate::VerificationOptions::disabled())
2133            .await
2134            .unwrap()
2135            .unwrap();
2136        let user2 = collection
2137            .get_with_verification("user2", &crate::VerificationOptions::disabled())
2138            .await
2139            .unwrap()
2140            .unwrap();
2141        assert_eq!(user1.data()["name"], "User1");
2142        assert_eq!(user2.data()["name"], "User2");
2143
2144        // Update one
2145        collection
2146            .update("user1", json!({"name": "Updated"}))
2147            .await
2148            .unwrap();
2149        let updated = collection
2150            .get_with_verification("user1", &crate::VerificationOptions::disabled())
2151            .await
2152            .unwrap()
2153            .unwrap();
2154        assert_eq!(updated.data()["name"], "Updated");
2155
2156        // Delete one
2157        collection.delete("user2").await.unwrap();
2158        assert!(collection
2159            .get_with_verification("user2", &crate::VerificationOptions::disabled())
2160            .await
2161            .unwrap()
2162            .is_none());
2163        assert!(collection
2164            .get_with_verification("user1", &crate::VerificationOptions::disabled())
2165            .await
2166            .unwrap()
2167            .is_some());
2168    }
2169
2170    #[test]
2171    fn test_validate_document_id_valid() {
2172        // Valid IDs
2173        assert!(validate_document_id("user-123").is_ok());
2174        assert!(validate_document_id("user_456").is_ok());
2175        assert!(validate_document_id("data-item").is_ok());
2176        assert!(validate_document_id("test_collection_123").is_ok());
2177        assert!(validate_document_id("file-txt").is_ok());
2178        assert!(validate_document_id("a").is_ok());
2179        assert!(validate_document_id("123").is_ok());
2180    }
2181
2182    #[test]
2183    fn test_validate_document_id_invalid_empty() {
2184        assert!(validate_document_id("").is_err());
2185    }
2186
2187    #[test]
2188    fn test_validate_document_id_invalid_path_separators() {
2189        assert!(validate_document_id("path/traversal").is_err());
2190        assert!(validate_document_id("path\\traversal").is_err());
2191    }
2192
2193    #[test]
2194    fn test_validate_document_id_invalid_control_characters() {
2195        assert!(validate_document_id("file\nname").is_err());
2196        assert!(validate_document_id("file\x00name").is_err());
2197    }
2198
2199    #[test]
2200    fn test_validate_document_id_invalid_windows_reserved_characters() {
2201        assert!(validate_document_id("file<name>").is_err());
2202        assert!(validate_document_id("file>name").is_err());
2203        assert!(validate_document_id("file:name").is_err());
2204        assert!(validate_document_id("file\"name").is_err());
2205        assert!(validate_document_id("file|name").is_err());
2206        assert!(validate_document_id("file?name").is_err());
2207        assert!(validate_document_id("file*name").is_err());
2208    }
2209
2210    #[test]
2211    fn test_validate_document_id_invalid_other_characters() {
2212        assert!(validate_document_id("file name").is_err()); // space
2213        assert!(validate_document_id("file@name").is_err()); // @
2214        assert!(validate_document_id("file!name").is_err()); // !
2215        assert!(validate_document_id("file🚀name").is_err()); // emoji
2216        assert!(validate_document_id("fileéname").is_err()); // accented
2217        assert!(validate_document_id("file.name").is_err()); // dot
2218    }
2219
2220    #[test]
2221    fn test_validate_document_id_invalid_windows_reserved_names() {
2222        // Test reserved names (case-insensitive)
2223        assert!(validate_document_id("CON").is_err());
2224        assert!(validate_document_id("con").is_err());
2225        assert!(validate_document_id("Con").is_err());
2226        assert!(validate_document_id("PRN").is_err());
2227        assert!(validate_document_id("AUX").is_err());
2228        assert!(validate_document_id("NUL").is_err());
2229        assert!(validate_document_id("COM1").is_err());
2230        assert!(validate_document_id("LPT9").is_err());
2231
2232        // Test with extensions
2233        assert!(validate_document_id("CON.txt").is_err());
2234        assert!(validate_document_id("prn.backup").is_err());
2235    }
2236
2237    #[tokio::test]
2238    async fn test_insert_invalid_document_id() {
2239        let (collection, _temp_dir) = setup_collection().await;
2240
2241        let doc = json!({ "data": "test" });
2242
2243        // Test empty ID
2244        assert!(collection.insert("", doc.clone()).await.is_err());
2245
2246        // Test Windows reserved name
2247        assert!(collection.insert("CON", doc.clone()).await.is_err());
2248
2249        // Test invalid character
2250        assert!(collection.insert("file name", doc.clone()).await.is_err());
2251    }
2252
2253    #[tokio::test]
2254    async fn test_get_corrupted_json() {
2255        let (collection, _temp_dir) = setup_collection().await;
2256
2257        // Manually create a file with invalid JSON
2258        let file_path = collection.path.join("corrupted.json");
2259        tokio_fs::write(&file_path, "{ invalid json }")
2260            .await
2261            .unwrap();
2262
2263        let result = collection.get("corrupted").await;
2264        assert!(result.is_err());
2265    }
2266
2267    #[tokio::test]
2268    async fn test_update_invalid_document_id() {
2269        let (collection, _temp_dir) = setup_collection().await;
2270
2271        let doc = json!({ "data": "test" });
2272
2273        // Test empty ID
2274        assert!(collection.update("", doc.clone()).await.is_err());
2275
2276        // Test Windows reserved name
2277        assert!(collection.update("CON", doc.clone()).await.is_err());
2278    }
2279
2280    #[tokio::test]
2281    async fn test_delete_invalid_document_id() {
2282        let (collection, _temp_dir) = setup_collection().await;
2283
2284        // Test empty ID
2285        assert!(collection.delete("").await.is_err());
2286
2287        // Test Windows reserved name
2288        assert!(collection.delete("CON").await.is_err());
2289    }
2290
2291    #[tokio::test]
2292    async fn test_get_nonexistent_with_verification() {
2293        let (collection, _temp_dir) = setup_collection().await;
2294
2295        let options = crate::VerificationOptions::strict();
2296        let result = collection
2297            .get_with_verification("nonexistent", &options)
2298            .await;
2299        assert!(result.is_ok());
2300        assert!(result.unwrap().is_none());
2301    }
2302
2303    #[tokio::test]
2304    async fn test_get_with_verification_disabled() {
2305        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2306
2307        let doc = json!({ "name": "Alice", "data": "test" });
2308        collection.insert("test_doc", doc.clone()).await.unwrap();
2309
2310        // Tamper with the file
2311        let file_path = collection.path.join("test_doc.json");
2312        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2313        content = content.replace("test", "tampered");
2314        tokio_fs::write(&file_path, &content).await.unwrap();
2315
2316        // Should succeed with verification disabled
2317        let options = crate::VerificationOptions::disabled();
2318        let result = collection.get_with_verification("test_doc", &options).await;
2319        assert!(result.is_ok());
2320        let doc = result.unwrap().unwrap();
2321        assert_eq!(doc.data()["name"], "Alice");
2322    }
2323
2324    #[tokio::test]
2325    async fn test_get_with_verification_empty_signature_warn() {
2326        let (collection, _temp_dir) = setup_collection().await;
2327
2328        // Insert unsigned document
2329        let doc = json!({ "name": "Unsigned" });
2330        collection
2331            .insert("unsigned_doc", doc.clone())
2332            .await
2333            .unwrap();
2334
2335        // Should warn but not fail
2336        let options = crate::VerificationOptions {
2337            verify_signature:            true,
2338            verify_hash:                 true,
2339            signature_verification_mode: crate::VerificationMode::Strict,
2340            empty_signature_mode:        crate::VerificationMode::Warn,
2341            hash_verification_mode:      crate::VerificationMode::Strict,
2342        };
2343        let result = collection
2344            .get_with_verification("unsigned_doc", &options)
2345            .await;
2346        assert!(result.is_ok());
2347        assert!(result.unwrap().is_some());
2348    }
2349
2350    #[tokio::test]
2351    async fn test_get_with_verification_empty_signature_strict() {
2352        let (collection, _temp_dir) = setup_collection().await;
2353
2354        // Insert unsigned document
2355        let doc = json!({ "name": "Unsigned" });
2356        collection
2357            .insert("unsigned_doc", doc.clone())
2358            .await
2359            .unwrap();
2360
2361        // Should fail with strict empty signature mode
2362        let options = crate::VerificationOptions {
2363            verify_signature:            true,
2364            verify_hash:                 true,
2365            signature_verification_mode: crate::VerificationMode::Strict,
2366            empty_signature_mode:        crate::VerificationMode::Strict,
2367            hash_verification_mode:      crate::VerificationMode::Strict,
2368        };
2369        let result = collection
2370            .get_with_verification("unsigned_doc", &options)
2371            .await;
2372        assert!(result.is_err());
2373        match result.unwrap_err() {
2374            crate::SentinelError::SignatureVerificationFailed {
2375                id,
2376                reason,
2377            } => {
2378                assert_eq!(id, "unsigned_doc");
2379                assert!(reason.contains("no signature"));
2380            },
2381            _ => panic!("Expected SignatureVerificationFailed"),
2382        }
2383    }
2384
2385    #[tokio::test]
2386    async fn test_all_empty_collection() {
2387        let (collection, _temp_dir) = setup_collection().await;
2388
2389        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2390        assert!(docs.is_empty());
2391    }
2392
2393    #[tokio::test]
2394    async fn test_all_with_multiple_documents() {
2395        let (collection, _temp_dir) = setup_collection().await;
2396
2397        for i in 0 .. 5 {
2398            let doc = json!({ "id": i, "name": format!("User{}", i) });
2399            collection
2400                .insert(&format!("user-{}", i), doc)
2401                .await
2402                .unwrap();
2403        }
2404
2405        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2406        assert_eq!(docs.len(), 5);
2407
2408        let ids: std::collections::HashSet<_> = docs.iter().map(|d| d.id().to_string()).collect();
2409        for i in 0 .. 5 {
2410            assert!(ids.contains(&format!("user-{}", i)));
2411        }
2412    }
2413
2414    #[tokio::test]
2415    async fn test_all_with_verification() {
2416        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2417
2418        for i in 0 .. 3 {
2419            let doc = json!({ "id": i });
2420            collection
2421                .insert(&format!("signed-{}", i), doc)
2422                .await
2423                .unwrap();
2424        }
2425
2426        let options = crate::VerificationOptions::strict();
2427        let docs: Vec<_> = collection
2428            .all_with_verification(&options)
2429            .try_collect()
2430            .await
2431            .unwrap();
2432        assert_eq!(docs.len(), 3);
2433    }
2434
2435    #[tokio::test]
2436    async fn test_filter_empty_result() {
2437        let (collection, _temp_dir) = setup_collection().await;
2438
2439        for i in 0 .. 3 {
2440            let doc = json!({ "id": i, "status": "active" });
2441            collection
2442                .insert(&format!("user-{}", i), doc)
2443                .await
2444                .unwrap();
2445        }
2446
2447        let results: Vec<_> = collection
2448            .filter(|doc| doc.data().get("status") == Some(&json!("inactive")))
2449            .try_collect()
2450            .await
2451            .unwrap();
2452        assert!(results.is_empty());
2453    }
2454
2455    #[tokio::test]
2456    async fn test_filter_with_all_matching() {
2457        let (collection, _temp_dir) = setup_collection().await;
2458
2459        for i in 0 .. 5 {
2460            let doc = json!({ "id": i, "active": true });
2461            collection
2462                .insert(&format!("user-{}", i), doc)
2463                .await
2464                .unwrap();
2465        }
2466
2467        let results: Vec<_> = collection
2468            .filter(|doc| doc.data().get("active") == Some(&json!(true)))
2469            .try_collect()
2470            .await
2471            .unwrap();
2472        assert_eq!(results.len(), 5);
2473    }
2474
2475    #[tokio::test]
2476    async fn test_filter_with_verification() {
2477        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2478
2479        for i in 0 .. 3 {
2480            let doc = json!({ "id": i, "active": true });
2481            collection
2482                .insert(&format!("signed-{}", i), doc)
2483                .await
2484                .unwrap();
2485        }
2486
2487        let options = crate::VerificationOptions::strict();
2488        let results: Vec<_> = collection
2489            .filter_with_verification(
2490                |doc| doc.data().get("active") == Some(&json!(true)),
2491                &options,
2492            )
2493            .try_collect()
2494            .await
2495            .unwrap();
2496        assert_eq!(results.len(), 3);
2497    }
2498
2499    #[tokio::test]
2500    async fn test_bulk_insert_empty_all() {
2501        let (collection, _temp_dir) = setup_collection().await;
2502
2503        let result = collection.bulk_insert(vec![]).await;
2504        assert!(result.is_ok());
2505
2506        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2507        assert!(docs.is_empty());
2508    }
2509
2510    #[tokio::test]
2511    async fn test_bulk_insert_large_batch() {
2512        let (collection, _temp_dir) = setup_collection().await;
2513
2514        let documents: Vec<(String, serde_json::Value)> = (0 .. 100)
2515            .map(|i| {
2516                let key = format!("user-{}", i);
2517                let value = json!({ "id": i, "data": format!("value{}", i) });
2518                (key, value)
2519            })
2520            .collect();
2521
2522        // Convert Vec<(String, Value)> to Vec<(&str, Value)>
2523        let documents_refs: Vec<(&str, serde_json::Value)> = documents
2524            .iter()
2525            .map(|(k, v)| (k.as_str(), v.clone()))
2526            .collect();
2527
2528        // This should trigger the debug log for bulk insert
2529        let result = collection.bulk_insert(documents_refs).await;
2530        assert!(result.is_ok());
2531
2532        // Verify all documents were inserted
2533        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2534        assert_eq!(docs.len(), 100);
2535    }
2536
2537    #[tokio::test]
2538    async fn test_bulk_insert_partial_failure() {
2539        let (collection, _temp_dir) = setup_collection().await;
2540
2541        let documents = vec![
2542            ("valid-1", json!({ "name": "One" })),
2543            ("valid-2", json!({ "name": "Two" })),
2544            ("invalid id!", json!({ "name": "Three" })), // This will fail
2545        ];
2546
2547        let result = collection.bulk_insert(documents).await;
2548        assert!(result.is_err());
2549
2550        // First two should not be inserted (transaction safety not implemented yet)
2551        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2552        assert!(docs.len() <= 2);
2553    }
2554
2555    #[tokio::test]
2556    async fn test_query_empty_filter() {
2557        let (collection, _temp_dir) = setup_collection().await;
2558
2559        for i in 0 .. 10 {
2560            let doc = json!({ "id": i, "value": i * 10 });
2561            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2562        }
2563
2564        let query = crate::QueryBuilder::new().build();
2565        let result = collection.query(query).await.unwrap();
2566        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2567        assert_eq!(docs.len(), 10);
2568    }
2569
2570    #[tokio::test]
2571    async fn test_query_with_limit() {
2572        let (collection, _temp_dir) = setup_collection().await;
2573
2574        for i in 0 .. 100 {
2575            let doc = json!({ "id": i });
2576            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2577        }
2578
2579        let query = crate::QueryBuilder::new().limit(5).build();
2580        let result = collection.query(query).await.unwrap();
2581        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2582        assert_eq!(docs.len(), 5);
2583    }
2584
2585    #[tokio::test]
2586    async fn test_query_with_offset() {
2587        let (collection, _temp_dir) = setup_collection().await;
2588
2589        for i in 0 .. 10 {
2590            let doc = json!({ "id": i, "value": i });
2591            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2592        }
2593
2594        let query = crate::QueryBuilder::new().offset(5).build();
2595        let result = collection.query(query).await.unwrap();
2596        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2597        assert_eq!(docs.len(), 5);
2598    }
2599
2600    #[tokio::test]
2601    async fn test_query_with_limit_and_offset() {
2602        let (collection, _temp_dir) = setup_collection().await;
2603
2604        for i in 0 .. 100 {
2605            let doc = json!({ "id": i, "value": i });
2606            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2607        }
2608
2609        let query = crate::QueryBuilder::new().offset(10).limit(5).build();
2610        let result = collection.query(query).await.unwrap();
2611        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2612        assert_eq!(docs.len(), 5);
2613    }
2614
2615    #[tokio::test]
2616    async fn test_query_with_sort_ascending() {
2617        let (collection, _temp_dir) = setup_collection().await;
2618
2619        for i in (0 .. 5).rev() {
2620            let doc = json!({ "id": i, "value": i });
2621            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2622        }
2623
2624        let query = crate::QueryBuilder::new()
2625            .sort("id", crate::SortOrder::Ascending)
2626            .build();
2627        let result = collection.query(query).await.unwrap();
2628        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2629
2630        assert_eq!(docs.len(), 5);
2631        for (i, doc) in docs.iter().enumerate() {
2632            assert_eq!(doc.data()["id"], json!(i));
2633        }
2634    }
2635
2636    #[tokio::test]
2637    async fn test_query_with_sort_descending() {
2638        let (collection, _temp_dir) = setup_collection().await;
2639
2640        for i in 0 .. 5 {
2641            let doc = json!({ "id": i, "value": i });
2642            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2643        }
2644
2645        let query = crate::QueryBuilder::new()
2646            .sort("id", crate::SortOrder::Descending)
2647            .build();
2648        let result = collection.query(query).await.unwrap();
2649        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2650
2651        assert_eq!(docs.len(), 5);
2652        for (i, doc) in docs.iter().enumerate() {
2653            assert_eq!(doc.data()["id"], json!(4 - i));
2654        }
2655    }
2656
2657    #[tokio::test]
2658    async fn test_query_with_projection() {
2659        let (collection, _temp_dir) = setup_collection().await;
2660
2661        for i in 0 .. 3 {
2662            let doc =
2663                json!({ "id": i, "name": format!("User{}", i), "email": format!("user{}@example.com", i), "age": 30 });
2664            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2665        }
2666
2667        let query = crate::QueryBuilder::new()
2668            .projection(vec!["id", "name"])
2669            .build();
2670        let result = collection.query(query).await.unwrap();
2671        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2672
2673        assert_eq!(docs.len(), 3);
2674        for doc in &docs {
2675            assert!(doc.data().get("id").is_some());
2676            assert!(doc.data().get("name").is_some());
2677            assert!(doc.data().get("email").is_none());
2678            assert!(doc.data().get("age").is_none());
2679        }
2680    }
2681
2682    #[tokio::test]
2683    async fn test_query_with_verification() {
2684        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2685
2686        for i in 0 .. 5 {
2687            let doc = json!({ "id": i, "active": true });
2688            collection
2689                .insert(&format!("signed-{}", i), doc)
2690                .await
2691                .unwrap();
2692        }
2693
2694        let options = crate::VerificationOptions::strict();
2695        let query = crate::QueryBuilder::new().build();
2696        let result = collection
2697            .query_with_verification(query, &options)
2698            .await
2699            .unwrap();
2700        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2701        assert_eq!(docs.len(), 5);
2702    }
2703
2704    #[tokio::test]
2705    async fn test_query_complex() {
2706        let (collection, _temp_dir) = setup_collection().await;
2707
2708        // Insert test data
2709        let test_data = vec![
2710            (
2711                "doc1",
2712                json!({ "name": "Alice", "age": 25, "city": "NYC", "active": true }),
2713            ),
2714            (
2715                "doc2",
2716                json!({ "name": "Bob", "age": 30, "city": "LA", "active": true }),
2717            ),
2718            (
2719                "doc3",
2720                json!({ "name": "Charlie", "age": 35, "city": "NYC", "active": false }),
2721            ),
2722            (
2723                "doc4",
2724                json!({ "name": "Diana", "age": 28, "city": "NYC", "active": true }),
2725            ),
2726            (
2727                "doc5",
2728                json!({ "name": "Eve", "age": 40, "city": "LA", "active": false }),
2729            ),
2730        ];
2731
2732        for (id, doc) in &test_data {
2733            collection.insert(id, doc.clone()).await.unwrap();
2734        }
2735
2736        // Query: active=true, city=NYC, age>=26, limit 2, sort age asc, project name,age
2737        let query = crate::QueryBuilder::new()
2738            .filter("active", crate::Operator::Equals, json!(true))
2739            .filter("city", crate::Operator::Equals, json!("NYC"))
2740            .filter("age", crate::Operator::GreaterOrEqual, json!(26))
2741            .sort("age", crate::SortOrder::Ascending)
2742            .limit(2)
2743            .projection(vec!["name", "age"])
2744            .build();
2745
2746        let result = collection.query(query).await.unwrap();
2747        let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2748
2749        assert_eq!(docs.len(), 1);
2750        // Diana is 28, Bob is 30 but in LA (filtered out by city=NYC)
2751        assert_eq!(docs[0].data()["name"], json!("Diana"));
2752    }
2753
2754    #[tokio::test]
2755    async fn test_delete_and_recover() {
2756        let (collection, _temp_dir) = setup_collection().await;
2757
2758        let doc = json!({ "name": "ToDelete" });
2759        collection.insert("test-doc", doc.clone()).await.unwrap();
2760
2761        // Verify it exists
2762        assert!(collection.get("test-doc").await.unwrap().is_some());
2763
2764        // Delete it
2765        collection.delete("test-doc").await.unwrap();
2766
2767        // Verify it's gone from main collection
2768        assert!(collection.get("test-doc").await.unwrap().is_none());
2769
2770        // Verify it's in .deleted/
2771        let deleted_path = collection.path.join(".deleted").join("test-doc.json");
2772        assert!(tokio_fs::try_exists(&deleted_path).await.unwrap());
2773
2774        // Recover it manually (no recover API yet)
2775        tokio_fs::rename(&deleted_path, collection.path.join("test-doc.json"))
2776            .await
2777            .unwrap();
2778
2779        // Verify it's back
2780        assert!(collection.get("test-doc").await.unwrap().is_some());
2781    }
2782
2783    #[tokio::test]
2784    async fn test_insert_special_characters_in_data() {
2785        let (collection, _temp_dir) = setup_collection().await;
2786
2787        let doc = json!({
2788            "string": "hello\nworld\ttab",
2789            "unicode": "Hello 世界 🌍",
2790            "null": null,
2791            "array": [1, 2, 3, "four"],
2792            "nested": { "deep": { "value": 42 } }
2793        });
2794
2795        collection.insert("special-doc", doc.clone()).await.unwrap();
2796
2797        let retrieved = collection.get("special-doc").await.unwrap().unwrap();
2798        assert_eq!(retrieved.data(), &doc);
2799    }
2800
2801    #[tokio::test]
2802    async fn test_insert_very_long_document_id() {
2803        let (collection, _temp_dir) = setup_collection().await;
2804
2805        // Use a reasonably long ID that works on most filesystems
2806        // (255 bytes may exceed some filesystem limits depending on path length)
2807        let long_id = "a".repeat(200);
2808        let doc = json!({ "data": "test" });
2809
2810        let result = collection.insert(&long_id, doc).await;
2811        assert!(result.is_ok());
2812
2813        let retrieved = collection.get(&long_id).await.unwrap().unwrap();
2814        assert_eq!(retrieved.id(), &long_id);
2815    }
2816
2817    #[tokio::test]
2818    async fn test_insert_max_value_numbers() {
2819        let (collection, _temp_dir) = setup_collection().await;
2820
2821        let doc = json!({
2822            "max_i64": 9223372036854775807i64,
2823            "min_i64": -9223372036854775808i64,
2824            "max_f64": 1.7976931348623157e308,
2825            "min_f64": -1.7976931348623157e308
2826        });
2827
2828        collection.insert("numbers", doc.clone()).await.unwrap();
2829
2830        let retrieved = collection.get("numbers").await.unwrap().unwrap();
2831        assert_eq!(retrieved.data(), &doc);
2832    }
2833
2834    #[tokio::test]
2835    async fn test_insert_nested_array_document() {
2836        let (collection, _temp_dir) = setup_collection().await;
2837
2838        let doc = json!({
2839            "matrix": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
2840            "mixed": [1, "two", true, null, { "nested": "value" }]
2841        });
2842
2843        collection.insert("arrays", doc.clone()).await.unwrap();
2844
2845        let retrieved = collection.get("arrays").await.unwrap().unwrap();
2846        assert_eq!(retrieved.data(), &doc);
2847    }
2848
2849    #[tokio::test]
2850    async fn test_collection_name() {
2851        let (collection, _temp_dir) = setup_collection().await;
2852
2853        assert_eq!(collection.name(), "test_collection");
2854    }
2855
2856    #[tokio::test]
2857    async fn test_verify_hash_valid() {
2858        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2859
2860        let doc = json!({ "name": "Test" });
2861        collection.insert("hash-test", doc.clone()).await.unwrap();
2862
2863        let retrieved = collection.get("hash-test").await.unwrap().unwrap();
2864        let options = crate::VerificationOptions {
2865            verify_signature:            false,
2866            verify_hash:                 true,
2867            signature_verification_mode: crate::VerificationMode::Strict,
2868            empty_signature_mode:        crate::VerificationMode::Warn,
2869            hash_verification_mode:      crate::VerificationMode::Strict,
2870        };
2871
2872        let result = collection.verify_document(&retrieved, &options).await;
2873        assert!(result.is_ok());
2874    }
2875
2876    #[tokio::test]
2877    async fn test_verify_hash_invalid() {
2878        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2879
2880        let doc = json!({ "name": "Original" });
2881        collection
2882            .insert("hash-invalid", doc.clone())
2883            .await
2884            .unwrap();
2885
2886        // Tamper with the file
2887        let file_path = collection.path.join("hash-invalid.json");
2888        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2889        content = content.replace("Original", "Tampered");
2890        tokio_fs::write(&file_path, &content).await.unwrap();
2891
2892        // Re-read the document (disable verification to read the tampered file)
2893        let retrieved = collection
2894            .get_with_verification("hash-invalid", &crate::VerificationOptions::disabled())
2895            .await
2896            .unwrap()
2897            .unwrap();
2898
2899        let options = crate::VerificationOptions {
2900            verify_signature:            false,
2901            verify_hash:                 true,
2902            signature_verification_mode: crate::VerificationMode::Strict,
2903            empty_signature_mode:        crate::VerificationMode::Warn,
2904            hash_verification_mode:      crate::VerificationMode::Strict,
2905        };
2906
2907        let result = collection.verify_document(&retrieved, &options).await;
2908        assert!(result.is_err());
2909        match result.unwrap_err() {
2910            crate::SentinelError::HashVerificationFailed {
2911                id,
2912                ..
2913            } => {
2914                assert_eq!(id, "hash-invalid");
2915            },
2916            _ => panic!("Expected HashVerificationFailed"),
2917        }
2918    }
2919
2920    #[tokio::test]
2921    async fn test_verify_signature_valid() {
2922        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2923
2924        let doc = json!({ "name": "Signed" });
2925        collection
2926            .insert("signed-valid", doc.clone())
2927            .await
2928            .unwrap();
2929
2930        let retrieved = collection.get("signed-valid").await.unwrap().unwrap();
2931        let options = crate::VerificationOptions::strict();
2932
2933        let result = collection.verify_document(&retrieved, &options).await;
2934        assert!(result.is_ok());
2935    }
2936
2937    #[tokio::test]
2938    async fn test_verify_signature_invalid() {
2939        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2940
2941        let doc = json!({ "name": "Original" });
2942        collection.insert("sig-invalid", doc.clone()).await.unwrap();
2943
2944        // Tamper with the file
2945        let file_path = collection.path.join("sig-invalid.json");
2946        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2947        content = content.replace("Original", "Tampered");
2948        tokio_fs::write(&file_path, &content).await.unwrap();
2949
2950        // Re-read the document (disable verification to read the tampered file)
2951        let retrieved = collection
2952            .get_with_verification("sig-invalid", &crate::VerificationOptions::disabled())
2953            .await
2954            .unwrap()
2955            .unwrap();
2956        let options = crate::VerificationOptions::strict();
2957
2958        let result = collection.verify_document(&retrieved, &options).await;
2959        assert!(result.is_err());
2960    }
2961
2962    #[tokio::test]
2963    async fn test_insert_unsigned_document() {
2964        // Test inserting document without signing key to cover line 147-148
2965        let temp_dir = tempfile::tempdir().unwrap();
2966        let store = Store::new(temp_dir.path(), None).await.unwrap();
2967        let collection = store.collection("test").await.unwrap();
2968
2969        let data = json!({ "name": "test" });
2970        let result = collection.insert("unsigned-doc", data).await;
2971        assert!(result.is_ok());
2972
2973        let doc = collection.get("unsigned-doc").await.unwrap().unwrap();
2974        assert_eq!(doc.data()["name"], "test");
2975    }
2976
2977    #[tokio::test]
2978    async fn test_delete_nonexistent_document() {
2979        // Test deleting a document that doesn't exist to cover line 371-374
2980        let (collection, _temp_dir) = setup_collection().await;
2981
2982        // Try to delete a document that was never created
2983        let result = collection.delete("nonexistent-doc").await;
2984        assert!(result.is_ok()); // Should succeed silently
2985    }
2986
2987    #[tokio::test]
2988    async fn test_delete_soft_delete_path() {
2989        // Test soft delete to cover line 358-359
2990        let (collection, temp_dir) = setup_collection().await;
2991
2992        // Insert a document
2993        let data = json!({ "name": "to-delete" });
2994        collection.insert("doc-to-delete", data).await.unwrap();
2995
2996        // Delete it
2997        let result = collection.delete("doc-to-delete").await;
2998        assert!(result.is_ok());
2999
3000        // Verify it's in .deleted directory
3001        let deleted_path = temp_dir
3002            .path()
3003            .join("data/test_collection/.deleted/doc-to-delete.json");
3004        assert!(tokio::fs::metadata(&deleted_path).await.is_ok());
3005    }
3006
3007    #[tokio::test]
3008    async fn test_streaming_all_skips_deleted() {
3009        let (collection, _temp_dir) = setup_collection().await;
3010
3011        for i in 0 .. 5 {
3012            let doc = json!({ "id": i });
3013            collection.insert(&format!("doc-{}", i), doc).await.unwrap();
3014        }
3015
3016        // Delete some
3017        collection.delete("doc-1").await.unwrap();
3018        collection.delete("doc-3").await.unwrap();
3019
3020        let docs: Vec<_> = collection.all().try_collect().await.unwrap();
3021        assert_eq!(docs.len(), 3);
3022
3023        let ids: std::collections::HashSet<_> = docs.iter().map(|d| d.id().to_string()).collect();
3024        assert!(ids.contains("doc-0"));
3025        assert!(!ids.contains("doc-1"));
3026        assert!(ids.contains("doc-2"));
3027        assert!(!ids.contains("doc-3"));
3028        assert!(ids.contains("doc-4"));
3029    }
3030
3031    #[tokio::test]
3032    async fn test_count_method() {
3033        // Test line 449-452: count() method trace logs
3034        let (collection, _temp_dir) = setup_collection().await;
3035
3036        collection
3037            .insert("doc-1", json!({"data": 1}))
3038            .await
3039            .unwrap();
3040        collection
3041            .insert("doc-2", json!({"data": 2}))
3042            .await
3043            .unwrap();
3044
3045        let count = collection.count().await.unwrap();
3046        assert_eq!(count, 2);
3047    }
3048
3049    #[tokio::test]
3050    async fn test_get_many() {
3051        // Test lines 1467-1468, 1470, 1472, 1476: get_many batch retrieval
3052        let (collection, _temp_dir) = setup_collection().await;
3053
3054        collection.insert("doc-1", json!({"id": 1})).await.unwrap();
3055        collection.insert("doc-2", json!({"id": 2})).await.unwrap();
3056
3057        let ids = vec!["doc-1", "doc-2", "non-existent"];
3058        let results = collection.get_many(&ids).await.unwrap();
3059
3060        assert_eq!(results.len(), 3);
3061        assert!(results[0].is_some());
3062        assert!(results[1].is_some());
3063        assert!(results[2].is_none());
3064    }
3065
3066    #[tokio::test]
3067    async fn test_upsert_insert() {
3068        // Test lines 1531-1533: upsert creates new document
3069        let (collection, _temp_dir) = setup_collection().await;
3070
3071        let is_new = collection
3072            .upsert("new-doc", json!({"value": 100}))
3073            .await
3074            .unwrap();
3075        assert!(is_new);
3076
3077        let doc = collection.get("new-doc").await.unwrap().unwrap();
3078        assert_eq!(doc.data().get("value").unwrap(), &json!(100));
3079    }
3080
3081    #[tokio::test]
3082    async fn test_upsert_update() {
3083        // Test lines 1523, 1525-1527: upsert updates existing document
3084        let (collection, _temp_dir) = setup_collection().await;
3085
3086        collection
3087            .insert("existing", json!({"value": 1}))
3088            .await
3089            .unwrap();
3090        let is_new = collection
3091            .upsert("existing", json!({"value": 2}))
3092            .await
3093            .unwrap();
3094
3095        assert!(!is_new);
3096        let doc = collection.get("existing").await.unwrap().unwrap();
3097        assert_eq!(doc.data().get("value").unwrap(), &json!(2));
3098    }
3099
3100    #[tokio::test]
3101    async fn test_aggregate_count() {
3102        // Test line 1601: aggregate count
3103        let (collection, _temp_dir) = setup_collection().await;
3104
3105        collection
3106            .insert("doc-1", json!({"value": 1}))
3107            .await
3108            .unwrap();
3109        collection
3110            .insert("doc-2", json!({"value": 2}))
3111            .await
3112            .unwrap();
3113
3114        let result = collection
3115            .aggregate(vec![], crate::Aggregation::Count)
3116            .await
3117            .unwrap();
3118        assert_eq!(result, json!(2));
3119    }
3120
3121    #[tokio::test]
3122    async fn test_aggregate_sum() {
3123        // Test lines 1594-1596: aggregate sum
3124        let (collection, _temp_dir) = setup_collection().await;
3125
3126        collection
3127            .insert("doc-1", json!({"amount": 10}))
3128            .await
3129            .unwrap();
3130        collection
3131            .insert("doc-2", json!({"amount": 20}))
3132            .await
3133            .unwrap();
3134
3135        let result = collection
3136            .aggregate(vec![], crate::Aggregation::Sum("amount".to_string()))
3137            .await
3138            .unwrap();
3139        assert_eq!(result, json!(30.0));
3140    }
3141
3142    #[tokio::test]
3143    async fn test_aggregate_avg() {
3144        // Test lines 1609-1612: aggregate average
3145        let (collection, _temp_dir) = setup_collection().await;
3146
3147        collection
3148            .insert("doc-1", json!({"score": 10}))
3149            .await
3150            .unwrap();
3151        collection
3152            .insert("doc-2", json!({"score": 20}))
3153            .await
3154            .unwrap();
3155        collection
3156            .insert("doc-3", json!({"score": 30}))
3157            .await
3158            .unwrap();
3159
3160        let result = collection
3161            .aggregate(vec![], crate::Aggregation::Avg("score".to_string()))
3162            .await
3163            .unwrap();
3164        assert_eq!(result, json!(20.0));
3165    }
3166
3167    #[tokio::test]
3168    async fn test_aggregate_avg_no_docs() {
3169        // Test lines 1604-1606: average with no documents
3170        let (collection, _temp_dir) = setup_collection().await;
3171
3172        let result = collection
3173            .aggregate(vec![], crate::Aggregation::Avg("score".to_string()))
3174            .await
3175            .unwrap();
3176        assert_eq!(result, json!(null));
3177    }
3178
3179    #[tokio::test]
3180    async fn test_aggregate_min() {
3181        // Test lines 1621-1622: aggregate min
3182        let (collection, _temp_dir) = setup_collection().await;
3183
3184        collection
3185            .insert("doc-1", json!({"value": 15}))
3186            .await
3187            .unwrap();
3188        collection
3189            .insert("doc-2", json!({"value": 5}))
3190            .await
3191            .unwrap();
3192        collection
3193            .insert("doc-3", json!({"value": 10}))
3194            .await
3195            .unwrap();
3196
3197        let result = collection
3198            .aggregate(vec![], crate::Aggregation::Min("value".to_string()))
3199            .await
3200            .unwrap();
3201        assert_eq!(result, json!(5.0));
3202    }
3203
3204    #[tokio::test]
3205    async fn test_aggregate_min_no_values() {
3206        // Test lines 1617-1619: min with no numeric values
3207        let (collection, _temp_dir) = setup_collection().await;
3208
3209        collection
3210            .insert("doc-1", json!({"name": "test"}))
3211            .await
3212            .unwrap();
3213
3214        let result = collection
3215            .aggregate(vec![], crate::Aggregation::Min("value".to_string()))
3216            .await
3217            .unwrap();
3218        assert_eq!(result, json!(null));
3219    }
3220
3221    #[tokio::test]
3222    async fn test_aggregate_max() {
3223        // Test line 1633: aggregate max
3224        let (collection, _temp_dir) = setup_collection().await;
3225
3226        collection
3227            .insert("doc-1", json!({"value": 15}))
3228            .await
3229            .unwrap();
3230        collection
3231            .insert("doc-2", json!({"value": 25}))
3232            .await
3233            .unwrap();
3234        collection
3235            .insert("doc-3", json!({"value": 10}))
3236            .await
3237            .unwrap();
3238
3239        let result = collection
3240            .aggregate(vec![], crate::Aggregation::Max("value".to_string()))
3241            .await
3242            .unwrap();
3243        assert_eq!(result, json!(25.0));
3244    }
3245
3246    #[tokio::test]
3247    async fn test_aggregate_max_no_values() {
3248        // Test lines 1629-1630: max with no numeric values
3249        let (collection, _temp_dir) = setup_collection().await;
3250
3251        let result = collection
3252            .aggregate(vec![], crate::Aggregation::Max("value".to_string()))
3253            .await
3254            .unwrap();
3255        assert_eq!(result, json!(null));
3256    }
3257
3258    #[tokio::test]
3259    async fn test_aggregate_with_filters() {
3260        // Test lines 1587-1590, 1592: aggregation with filters
3261        let (collection, _temp_dir) = setup_collection().await;
3262
3263        collection
3264            .insert("doc-1", json!({"category": "A", "value": 10}))
3265            .await
3266            .unwrap();
3267        collection
3268            .insert("doc-2", json!({"category": "B", "value": 20}))
3269            .await
3270            .unwrap();
3271        collection
3272            .insert("doc-3", json!({"category": "A", "value": 15}))
3273            .await
3274            .unwrap();
3275
3276        let filters = vec![crate::Filter::Equals("category".to_string(), json!("A"))];
3277        let result = collection
3278            .aggregate(filters, crate::Aggregation::Sum("value".to_string()))
3279            .await
3280            .unwrap();
3281        assert_eq!(result, json!(25.0));
3282    }
3283
3284    #[tokio::test]
3285    async fn test_update_not_found() {
3286        // Test line 1396: update non-existent document
3287        let (collection, _temp_dir) = setup_collection().await;
3288
3289        let result = collection
3290            .update("non-existent", json!({"data": "value"}))
3291            .await;
3292        assert!(matches!(
3293            result,
3294            Err(crate::SentinelError::DocumentNotFound { .. })
3295        ));
3296    }
3297
3298    #[tokio::test]
3299    async fn test_update_merge_json_non_object() {
3300        // Test line 1364: merge when new value is not an object
3301        let (collection, _temp_dir) = setup_collection().await;
3302
3303        collection
3304            .insert("doc-1", json!({"name": "old"}))
3305            .await
3306            .unwrap();
3307        collection
3308            .update("doc-1", json!("simple string"))
3309            .await
3310            .unwrap();
3311
3312        let doc = collection.get("doc-1").await.unwrap().unwrap();
3313        assert_eq!(doc.data(), &json!("simple string"));
3314    }
3315
3316    #[tokio::test]
3317    async fn test_extract_numeric_value() {
3318        // Test lines 1369-1373: extract numeric value helper
3319        let (collection, _temp_dir) = setup_collection().await;
3320
3321        collection
3322            .insert("doc-1", json!({"price": 99.99, "name": "Product"}))
3323            .await
3324            .unwrap();
3325        let doc = collection.get("doc-1").await.unwrap().unwrap();
3326
3327        let price = Collection::extract_numeric_value(&doc, "price");
3328        assert_eq!(price, Some(99.99));
3329
3330        let name = Collection::extract_numeric_value(&doc, "name");
3331        assert_eq!(name, None);
3332
3333        let missing = Collection::extract_numeric_value(&doc, "missing_field");
3334        assert_eq!(missing, None);
3335    }
3336
3337    #[tokio::test]
3338    async fn test_delete_non_existent() {
3339        // Test lines 371-374: delete non-existent document
3340        let (collection, _temp_dir) = setup_collection().await;
3341
3342        // Should succeed (idempotent)
3343        let result = collection.delete("does-not-exist").await;
3344        assert!(result.is_ok());
3345    }
3346
3347    #[tokio::test]
3348    async fn test_update_unsigned_document() {
3349        // Test lines 1409-1410, 1413, 1417: update document without signing key
3350        let temp_dir = tempdir().unwrap();
3351        let store = Store::new(temp_dir.path(), None).await.unwrap();
3352        let collection = store.collection("test").await.unwrap();
3353
3354        collection
3355            .insert("doc-1", json!({"count": 1}))
3356            .await
3357            .unwrap();
3358        collection
3359            .update("doc-1", json!({"count": 2}))
3360            .await
3361            .unwrap();
3362
3363        let doc = collection.get("doc-1").await.unwrap().unwrap();
3364        assert_eq!(doc.data().get("count").unwrap(), &json!(2));
3365        assert_eq!(doc.signature(), ""); // No signature for unsigned docs
3366    }
3367
3368    #[tokio::test]
3369    async fn test_filter_with_malformed_json() {
3370        // Test lines 691, 694: Parse error in filter_with_verification stream
3371        use tokio::fs as tokio_fs;
3372        let (collection, _temp_dir) = setup_collection().await;
3373
3374        // Insert valid document first
3375        collection
3376            .insert("valid-doc", json!({"data": "valid"}))
3377            .await
3378            .unwrap();
3379
3380        // Create a malformed JSON file directly
3381        let malformed_path = collection.path.join("malformed.json");
3382        tokio_fs::write(&malformed_path, "{ this is not valid json }")
3383            .await
3384            .unwrap();
3385
3386        // Stream yields errors for malformed files
3387        let mut stream = collection.filter(|_| true);
3388        let mut found_valid = false;
3389        let mut found_error = false;
3390
3391        while let Some(result) = stream.next().await {
3392            match result {
3393                Ok(doc) if doc.id() == "valid-doc" => found_valid = true,
3394                Err(_) => found_error = true,
3395                _ => {},
3396            }
3397        }
3398
3399        assert!(found_valid);
3400        assert!(found_error); // Should encounter parse error
3401    }
3402
3403    #[tokio::test]
3404    async fn test_all_with_malformed_json() {
3405        // Test lines 834, 837: Parse error in all() stream
3406        use tokio::fs as tokio_fs;
3407        let (collection, _temp_dir) = setup_collection().await;
3408
3409        collection
3410            .insert("doc-1", json!({"data": "valid"}))
3411            .await
3412            .unwrap();
3413
3414        // Create malformed file
3415        let bad_path = collection.path.join("bad.json");
3416        tokio_fs::write(&bad_path, "not json at all").await.unwrap();
3417
3418        let mut stream = collection.all();
3419        let mut found_valid = false;
3420        let mut found_error = false;
3421
3422        while let Some(result) = stream.next().await {
3423            match result {
3424                Ok(doc) if doc.id() == "doc-1" => found_valid = true,
3425                Err(_) => found_error = true,
3426                _ => {},
3427            }
3428        }
3429
3430        assert!(found_valid);
3431        assert!(found_error);
3432    }
3433
3434    #[tokio::test]
3435    async fn test_query_with_malformed_json() {
3436        // Test lines 1120-1121: Parse error in query stream
3437        use tokio::fs as tokio_fs;
3438        let (collection, _temp_dir) = setup_collection().await;
3439
3440        collection
3441            .insert("valid", json!({"value": 42}))
3442            .await
3443            .unwrap();
3444
3445        // Create invalid document
3446        let invalid_path = collection.path.join("invalid.json");
3447        tokio_fs::write(&invalid_path, "{broken json}")
3448            .await
3449            .unwrap();
3450
3451        let query = crate::QueryBuilder::new().build();
3452        let result = collection.query(query).await.unwrap();
3453
3454        let mut stream = result.documents;
3455        let mut found_valid = false;
3456        let mut found_error = false;
3457
3458        while let Some(result) = stream.next().await {
3459            match result {
3460                Ok(doc) if doc.id() == "valid" => found_valid = true,
3461                Err(_) => found_error = true,
3462                _ => {},
3463            }
3464        }
3465
3466        assert!(found_valid);
3467        assert!(found_error);
3468    }
3469
3470    #[tokio::test]
3471    async fn test_filter_with_strict_hash_verification_failure() {
3472        // Test lines 678-679, 682-683: Strict mode hash verification failure
3473        use tokio::fs as tokio_fs;
3474        let (collection, _temp_dir) = setup_collection().await;
3475
3476        collection
3477            .insert("doc-1", json!({"data": "test"}))
3478            .await
3479            .unwrap();
3480
3481        // Corrupt the hash in the file
3482        let file_path = collection.path.join("doc-1.json");
3483        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3484        content = content.replace("\"hash\":", "\"hash\": \"corrupted_hash\", \"old_hash\":");
3485        tokio_fs::write(&file_path, content).await.unwrap();
3486
3487        let options = crate::VerificationOptions {
3488            verify_hash: true,
3489            hash_verification_mode: crate::VerificationMode::Strict,
3490            ..Default::default()
3491        };
3492
3493        let results: Result<Vec<_>> = collection
3494            .filter_with_verification(|_| true, &options)
3495            .try_collect()
3496            .await;
3497        assert!(results.is_err()); // Should fail in strict mode
3498    }
3499
3500    #[tokio::test]
3501    async fn test_all_with_strict_verification_failure() {
3502        // Test lines 823, 827: Strict mode verification in all() stream
3503        use tokio::fs as tokio_fs;
3504        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3505
3506        collection
3507            .insert("doc-1", json!({"data": "test"}))
3508            .await
3509            .unwrap();
3510
3511        // Corrupt the signature
3512        let file_path = collection.path.join("doc-1.json");
3513        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3514        content = content.replace(
3515            "\"signature\":",
3516            "\"signature\": \"bad_sig\", \"original_signature\":",
3517        );
3518        tokio_fs::write(&file_path, content).await.unwrap();
3519
3520        let options = crate::VerificationOptions {
3521            verify_signature: true,
3522            signature_verification_mode: crate::VerificationMode::Strict,
3523            ..Default::default()
3524        };
3525
3526        let results: Result<Vec<_>> = collection
3527            .all_with_verification(&options)
3528            .try_collect()
3529            .await;
3530        assert!(results.is_err());
3531    }
3532
3533    #[tokio::test]
3534    async fn test_query_with_strict_verification_failure() {
3535        // Test lines 1109, 1113: Strict verification in query
3536        use tokio::fs as tokio_fs;
3537        let (collection, _temp_dir) = setup_collection().await;
3538
3539        collection
3540            .insert("doc-1", json!({"value": 42}))
3541            .await
3542            .unwrap();
3543
3544        // Corrupt the hash
3545        let file_path = collection.path.join("doc-1.json");
3546        let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3547        content = content.replace("\"hash\":", "\"hash\": \"invalid_hash\", \"real_hash\":");
3548        tokio_fs::write(&file_path, content).await.unwrap();
3549
3550        let options = crate::VerificationOptions {
3551            verify_hash: true,
3552            hash_verification_mode: crate::VerificationMode::Strict,
3553            ..Default::default()
3554        };
3555
3556        let query = crate::QueryBuilder::new().build();
3557        let result = collection
3558            .query_with_verification(query, &options)
3559            .await
3560            .unwrap();
3561        let results: Result<Vec<_>> = result.documents.try_collect().await;
3562        assert!(results.is_err());
3563    }
3564
3565    #[tokio::test]
3566    async fn test_verify_hash_silent_mode() {
3567        // Test line 1167: Silent mode returns early (no-op)
3568        let (collection, _temp_dir) = setup_collection().await;
3569
3570        collection
3571            .insert("doc-1", json!({"data": "test"}))
3572            .await
3573            .unwrap();
3574        let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3575        doc.hash = "corrupted".to_string(); // Corrupt hash
3576
3577        let options = crate::VerificationOptions {
3578            verify_hash: true,
3579            hash_verification_mode: crate::VerificationMode::Silent,
3580            ..Default::default()
3581        };
3582
3583        let result = collection.verify_hash(&doc, options).await;
3584        assert!(result.is_ok()); // Silent mode doesn't fail
3585    }
3586
3587    #[tokio::test]
3588    async fn test_verify_hash_warn_mode_invalid() {
3589        // Test line 1189: Warn mode with invalid hash
3590        let (collection, _temp_dir) = setup_collection().await;
3591
3592        collection
3593            .insert("doc-1", json!({"data": "test"}))
3594            .await
3595            .unwrap();
3596        let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3597        doc.hash = "definitely_wrong".to_string();
3598
3599        let options = crate::VerificationOptions {
3600            verify_hash: true,
3601            hash_verification_mode: crate::VerificationMode::Warn,
3602            ..Default::default()
3603        };
3604
3605        let result = collection.verify_hash(&doc, options).await;
3606        assert!(result.is_ok()); // Warn mode doesn't fail, just warns
3607    }
3608
3609    #[tokio::test]
3610    async fn test_verify_hash_strict_mode_failure() {
3611        // Test lines 1214, 1216: Strict mode hash verification failure
3612        let (collection, _temp_dir) = setup_collection().await;
3613
3614        collection
3615            .insert("doc-1", json!({"data": "test"}))
3616            .await
3617            .unwrap();
3618        let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3619        doc.hash = "wrong_hash".to_string();
3620
3621        let options = crate::VerificationOptions {
3622            verify_hash: true,
3623            hash_verification_mode: crate::VerificationMode::Strict,
3624            ..Default::default()
3625        };
3626
3627        let result = collection.verify_hash(&doc, options).await;
3628        assert!(matches!(
3629            result,
3630            Err(crate::SentinelError::HashVerificationFailed { .. })
3631        ));
3632    }
3633
3634    #[tokio::test]
3635    async fn test_verify_signature_disabled() {
3636        // Test lines 1241-1242: Signature verification disabled
3637        let (collection, _temp_dir) = setup_collection().await;
3638
3639        collection
3640            .insert("doc-1", json!({"data": "test"}))
3641            .await
3642            .unwrap();
3643        let doc = collection.get("doc-1").await.unwrap().unwrap();
3644
3645        let options = crate::VerificationOptions {
3646            verify_signature: false,
3647            ..Default::default()
3648        };
3649
3650        let result = collection.verify_signature(&doc, options).await;
3651        assert!(result.is_ok());
3652    }
3653
3654    #[tokio::test]
3655    async fn test_verify_signature_strict_mode_failure() {
3656        // Test lines 1250, 1252, 1254: Strict mode signature verification failure
3657        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3658
3659        collection
3660            .insert("doc-1", json!({"data": "test"}))
3661            .await
3662            .unwrap();
3663        let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3664
3665        // Use a valid hex string but wrong signature value (all zeros)
3666        doc.signature = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string();
3667
3668        let options = crate::VerificationOptions {
3669            verify_signature: true,
3670            signature_verification_mode: crate::VerificationMode::Strict,
3671            ..Default::default()
3672        };
3673
3674        let result = collection.verify_signature(&doc, options).await;
3675        // Will fail because signature is wrong (even though hex is valid)
3676        assert!(result.is_err());
3677    }
3678
3679    #[tokio::test]
3680    async fn test_verify_signature_warn_mode_invalid() {
3681        // Test lines 1259-1261: Warn mode with invalid signature
3682        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3683
3684        collection
3685            .insert("doc-1", json!({"data": "test"}))
3686            .await
3687            .unwrap();
3688        collection
3689            .insert("doc-2", json!({"data": "different"}))
3690            .await
3691            .unwrap();
3692
3693        let mut doc1 = collection.get("doc-1").await.unwrap().unwrap();
3694        let doc2 = collection.get("doc-2").await.unwrap().unwrap();
3695
3696        // Use doc2's signature for doc1 (valid format but wrong signature)
3697        doc1.signature = doc2.signature().to_string();
3698
3699        let options = crate::VerificationOptions {
3700            verify_signature: true,
3701            signature_verification_mode: crate::VerificationMode::Warn,
3702            ..Default::default()
3703        };
3704
3705        let result = collection.verify_signature(&doc1, options).await;
3706        // Warn mode doesn't return error, just logs warning
3707        assert!(result.is_ok());
3708    }
3709
3710    #[tokio::test]
3711    async fn test_verify_signature_silent_mode() {
3712        // Test line 1265: Silent mode returns early
3713        let (collection, _temp_dir) = setup_collection().await;
3714
3715        collection
3716            .insert("doc-1", json!({"data": "test"}))
3717            .await
3718            .unwrap();
3719        let doc = collection.get("doc-1").await.unwrap().unwrap();
3720
3721        let options = crate::VerificationOptions {
3722            verify_signature: true,
3723            signature_verification_mode: crate::VerificationMode::Silent,
3724            empty_signature_mode: crate::VerificationMode::Silent,
3725            ..Default::default()
3726        };
3727
3728        let result = collection.verify_signature(&doc, options).await;
3729        assert!(result.is_ok());
3730    }
3731
3732    #[tokio::test]
3733    async fn test_verify_signature_success() {
3734        // Test line 1279: Signature verification success
3735        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3736
3737        collection
3738            .insert("doc-1", json!({"data": "test"}))
3739            .await
3740            .unwrap();
3741        let doc = collection.get("doc-1").await.unwrap().unwrap();
3742
3743        let options = crate::VerificationOptions {
3744            verify_signature: true,
3745            signature_verification_mode: crate::VerificationMode::Strict,
3746            ..Default::default()
3747        };
3748
3749        let result = collection.verify_signature(&doc, options).await;
3750        assert!(result.is_ok());
3751    }
3752
3753    #[tokio::test]
3754    async fn test_bulk_insert_trace_logs() {
3755        // Test line 499: bulk_insert trace logs
3756        let (collection, _temp_dir) = setup_collection().await;
3757
3758        let docs = vec![
3759            ("doc-1", json!({"value": 1})),
3760            ("doc-2", json!({"value": 2})),
3761        ];
3762
3763        collection.bulk_insert(docs).await.unwrap();
3764
3765        let doc1 = collection.get("doc-1").await.unwrap().unwrap();
3766        assert_eq!(doc1.data().get("value").unwrap(), &json!(1));
3767    }
3768
3769    #[tokio::test]
3770    async fn test_update_without_signing_key() {
3771        // Test lines 1396, 1409-1410, 1413, 1417: update path without signing key
3772        let temp_dir = tempfile::tempdir().unwrap();
3773
3774        // Create store and collection without signing key
3775        let store = Store::new(temp_dir.path(), None).await.unwrap();
3776        let collection = store.collection("test_collection").await.unwrap();
3777
3778        // Insert a document without signature (using the insert API directly)
3779        collection
3780            .insert("doc1", json!({"name": "Alice", "age": 30}))
3781            .await
3782            .unwrap();
3783
3784        // Update the document (this will use the path without signing key)
3785        collection.update("doc1", json!({"age": 31})).await.unwrap();
3786
3787        // Verify update succeeded
3788        let updated_doc = collection.get("doc1").await.unwrap().unwrap();
3789        assert_eq!(updated_doc.data()["age"], 31);
3790        assert_eq!(updated_doc.data()["name"], "Alice");
3791    }
3792
3793    #[tokio::test]
3794    async fn test_verify_signature_no_signing_key() {
3795        // Test line 1279: verification without signing key
3796        let temp_dir = tempfile::tempdir().unwrap();
3797
3798        // Create store and collection without signing key
3799        let store = Store::new(temp_dir.path(), None).await.unwrap();
3800        let collection = store.collection("test_collection").await.unwrap();
3801
3802        // Insert a document without signature
3803        collection
3804            .insert("doc1", json!({"name": "Alice"}))
3805            .await
3806            .unwrap();
3807
3808        // Get document and verify (should skip signature verification without key)
3809        let doc = collection.get("doc1").await.unwrap().unwrap();
3810        let options = crate::verification::VerificationOptions {
3811            verify_hash:                 true,
3812            verify_signature:            true,
3813            hash_verification_mode:      crate::VerificationMode::Strict,
3814            signature_verification_mode: crate::VerificationMode::Strict,
3815            empty_signature_mode:        crate::VerificationMode::Warn,
3816        };
3817
3818        // This should succeed since there's no signing key to verify against (line 1279)
3819        collection.verify_document(&doc, &options).await.unwrap();
3820    }
3821
3822    #[tokio::test]
3823    async fn test_update_with_signing_key() {
3824        // Test line 1396: update path WITH signing key
3825        let temp_dir = tempfile::tempdir().unwrap();
3826
3827        // Create store and collection WITH signing key
3828        let store = Store::new(temp_dir.path(), Some("test_passphrase"))
3829            .await
3830            .unwrap();
3831        let collection = store.collection("test_collection").await.unwrap();
3832
3833        // Insert a document with signature
3834        collection
3835            .insert("doc1", json!({"name": "Alice", "age": 30}))
3836            .await
3837            .unwrap();
3838
3839        // Update the document (this will use the path WITH signing key)
3840        collection.update("doc1", json!({"age": 31})).await.unwrap();
3841
3842        // Verify update succeeded
3843        let updated_doc = collection.get("doc1").await.unwrap().unwrap();
3844        assert_eq!(updated_doc.data()["age"], 31);
3845        assert_eq!(updated_doc.data()["name"], "Alice");
3846    }
3847}