Skip to main content

ipfrs_storage/
snapshot_manager.rs

1//! `StorageSnapshotManager` — Point-in-time snapshot management for storage
2//! state with incremental deltas and restoration.
3//!
4//! # Overview
5//!
6//! This module provides two distinct APIs:
7//!
8//! 1. **New production-grade API** (`StorageSnapshotManager`, `SnapshotId`,
9//!    `SnapshotEntry`, `SnapshotDelta`, `SsmSnapshot`, `StorageState`,
10//!    `SnapshotError`, `SnapshotStats`) — full point-in-time snapshot
11//!    management with FNV-1a checksums, incremental deltas, and chain-based
12//!    restoration.
13//!
14//! 2. **Legacy block-snapshot API** (`LegacySnapshotEntry`, `fnv1a_64`,
15//!    `LegacySnapshot`, `SnapshotKind`, `StorageSnapshot`, `SnapshotConfig`,
16//!    `SnapshotManagerStats`) — retained for backwards compatibility.
17
18use std::collections::{HashMap, VecDeque};
19use thiserror::Error;
20
21// ---------------------------------------------------------------------------
22// SnapshotId
23// ---------------------------------------------------------------------------
24
25/// Monotonically increasing snapshot identifier.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct SnapshotId(pub u64);
28
29impl std::fmt::Display for SnapshotId {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "SnapshotId({})", self.0)
32    }
33}
34
35// ---------------------------------------------------------------------------
36// SnapshotEntry
37// ---------------------------------------------------------------------------
38
39/// A key-value record at snapshot time.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SnapshotEntry {
42    /// The storage key.
43    pub key: String,
44    /// The raw value bytes.
45    pub value: Vec<u8>,
46    /// Logical version counter, incremented on each write to the same key.
47    pub version: u64,
48}
49
50// ---------------------------------------------------------------------------
51// SnapshotDelta
52// ---------------------------------------------------------------------------
53
54/// Changes relative to the previous snapshot.
55#[derive(Clone, Debug, Default, PartialEq, Eq)]
56pub struct SnapshotDelta {
57    /// Entries that were added since the previous snapshot.
58    pub added: Vec<SnapshotEntry>,
59    /// Entries that were modified since the previous snapshot.
60    pub modified: Vec<SnapshotEntry>,
61    /// Keys that were deleted since the previous snapshot.
62    pub deleted: Vec<String>,
63}
64
65impl SnapshotDelta {
66    /// Returns `true` if the delta contains no changes at all.
67    pub fn is_empty(&self) -> bool {
68        self.added.is_empty() && self.modified.is_empty() && self.deleted.is_empty()
69    }
70}
71
72// ---------------------------------------------------------------------------
73// SsmSnapshot (new Snapshot type)
74// ---------------------------------------------------------------------------
75
76/// An immutable point-in-time snapshot of the storage state.
77#[derive(Clone, Debug)]
78pub struct SsmSnapshot {
79    /// Unique monotonically-increasing identifier.
80    pub id: SnapshotId,
81    /// Unix timestamp (or logical clock) at which the snapshot was taken.
82    pub created_at: u64,
83    /// Number of live entries captured.
84    pub entry_count: usize,
85    /// Sum of all value byte lengths at snapshot time.
86    pub total_bytes: u64,
87    /// `None` for the first (full) snapshot; `Some` for incremental snapshots.
88    pub delta: Option<SnapshotDelta>,
89    /// FNV-1a checksum computed over all key+value pairs sorted by key.
90    pub checksum: u64,
91    /// Optional human-readable label.
92    pub label: Option<String>,
93    /// The full reconstructed state at this snapshot point (used for chain
94    /// restoration without having to replay from the very beginning each time).
95    ///
96    /// Stored inline so that `restore_snapshot` is O(1) after chain walk.
97    pub(crate) state_at_snapshot: HashMap<String, SnapshotEntry>,
98}
99
100// ---------------------------------------------------------------------------
101// StorageState
102// ---------------------------------------------------------------------------
103
104/// The current live mutable storage state.
105#[derive(Clone, Debug, Default)]
106pub struct StorageState {
107    /// Active key-value entries.
108    pub entries: HashMap<String, SnapshotEntry>,
109}
110
111// ---------------------------------------------------------------------------
112// SnapshotError
113// ---------------------------------------------------------------------------
114
115/// Errors returned by `StorageSnapshotManager` operations.
116#[derive(Debug, Error, PartialEq, Eq)]
117pub enum SnapshotError {
118    /// The requested snapshot id was not found.
119    #[error("snapshot not found: {0}")]
120    SnapshotNotFound(u64),
121
122    /// Attempted to delete a snapshot that is not the oldest.
123    #[error("only the oldest snapshot may be deleted")]
124    CannotDeleteNonOldest,
125
126    /// Operation requires at least one snapshot but none exist.
127    #[error("snapshot chain is empty")]
128    EmptySnapshotChain,
129
130    /// The delta chain is broken (an incremental snapshot references a missing
131    /// predecessor).
132    #[error("delta chain is broken")]
133    DeltaChainBroken,
134}
135
136// ---------------------------------------------------------------------------
137// SnapshotStats
138// ---------------------------------------------------------------------------
139
140/// Aggregate statistics for `StorageSnapshotManager`.
141#[derive(Clone, Debug, Default)]
142pub struct SnapshotStats {
143    /// Total number of snapshots retained.
144    pub snapshot_count: usize,
145    /// Id of the oldest retained snapshot, if any.
146    pub oldest_snapshot_id: Option<u64>,
147    /// Id of the newest retained snapshot, if any.
148    pub newest_snapshot_id: Option<u64>,
149    /// Sum of `total_bytes` across all retained snapshots.
150    pub total_snapshot_bytes: u64,
151    /// Number of entries in the current live state.
152    pub live_entries: usize,
153    /// Sum of value sizes in the current live state.
154    pub live_bytes: u64,
155}
156
157// ---------------------------------------------------------------------------
158// StorageSnapshotManager
159// ---------------------------------------------------------------------------
160
161/// Production-grade point-in-time snapshot manager with incremental deltas
162/// and chain-based restoration.
163pub struct StorageSnapshotManager {
164    /// The current live mutable state.
165    state: StorageState,
166    /// Ordered queue of retained snapshots (oldest first).
167    snapshots: VecDeque<SsmSnapshot>,
168    /// Maximum number of snapshots to retain before evicting the oldest.
169    max_snapshots: usize,
170    /// Next snapshot id to assign.
171    next_id: u64,
172    /// Next version counter to assign when writing entries.
173    next_version: u64,
174}
175
176// ---------------------------------------------------------------------------
177// Internal FNV-1a checksum helper
178// ---------------------------------------------------------------------------
179
180/// Compute an FNV-1a 64-bit checksum over all entries sorted by key.
181fn compute_checksum(entries: &HashMap<String, SnapshotEntry>) -> u64 {
182    let mut keys: Vec<&str> = entries.keys().map(|s| s.as_str()).collect();
183    keys.sort_unstable();
184    let mut h: u64 = 14_695_981_039_346_656_037;
185    for k in keys {
186        for b in k.bytes() {
187            h ^= u64::from(b);
188            h = h.wrapping_mul(1_099_511_628_211);
189        }
190        if let Some(e) = entries.get(k) {
191            for b in &e.value {
192                h ^= u64::from(*b);
193                h = h.wrapping_mul(1_099_511_628_211);
194            }
195        }
196    }
197    h
198}
199
200/// Reconstruct a `HashMap<String, SnapshotEntry>` state from a snapshot.
201/// For full snapshots (no delta), returns the stored state directly.
202/// For incremental snapshots, the state was already pre-computed and stored.
203fn reconstructed_state(snap: &SsmSnapshot) -> &HashMap<String, SnapshotEntry> {
204    &snap.state_at_snapshot
205}
206
207impl StorageSnapshotManager {
208    // -----------------------------------------------------------------------
209    // Construction
210    // -----------------------------------------------------------------------
211
212    /// Create a new, empty manager.
213    ///
214    /// `max_snapshots` controls how many snapshots are retained before the
215    /// oldest is evicted to make room for a new one.
216    pub fn new(max_snapshots: usize) -> Self {
217        Self {
218            state: StorageState::default(),
219            snapshots: VecDeque::new(),
220            max_snapshots: max_snapshots.max(1),
221            next_id: 1,
222            next_version: 1,
223        }
224    }
225
226    // -----------------------------------------------------------------------
227    // Live-state mutations
228    // -----------------------------------------------------------------------
229
230    /// Insert or update a key in the live state.
231    ///
232    /// The entry's `version` is set to `self.next_version` (then incremented).
233    pub fn put(&mut self, key: String, value: Vec<u8>, _now: u64) {
234        let version = self.next_version;
235        self.next_version += 1;
236        self.state.entries.insert(
237            key.clone(),
238            SnapshotEntry {
239                key,
240                value,
241                version,
242            },
243        );
244    }
245
246    /// Remove a key from the live state.
247    ///
248    /// Returns `false` if the key was not present.
249    pub fn delete(&mut self, key: &str) -> bool {
250        self.state.entries.remove(key).is_some()
251    }
252
253    /// Look up the value bytes for a key in the live state.
254    pub fn get(&self, key: &str) -> Option<&[u8]> {
255        self.state.entries.get(key).map(|e| e.value.as_slice())
256    }
257
258    // -----------------------------------------------------------------------
259    // Snapshot creation
260    // -----------------------------------------------------------------------
261
262    /// Capture the current live state as a new snapshot.
263    ///
264    /// - If this is the first snapshot, it is a *full* snapshot (`delta` is
265    ///   `None`).
266    /// - Otherwise, a delta relative to the preceding snapshot is computed.
267    ///
268    /// If the queue is already at `max_snapshots`, the oldest snapshot is
269    /// evicted before appending the new one.
270    pub fn take_snapshot(&mut self, label: Option<String>, now: u64) -> SnapshotId {
271        let id = SnapshotId(self.next_id);
272        self.next_id += 1;
273
274        // Clone the current live state for the snapshot.
275        let current_state = self.state.entries.clone();
276
277        // Compute delta vs. the previous snapshot (if any).
278        let delta: Option<SnapshotDelta> = if let Some(prev) = self.snapshots.back() {
279            let prev_state = reconstructed_state(prev);
280            Some(compute_delta(prev_state, &current_state))
281        } else {
282            None
283        };
284
285        let checksum = compute_checksum(&current_state);
286        let entry_count = current_state.len();
287        let total_bytes: u64 = current_state.values().map(|e| e.value.len() as u64).sum();
288
289        let snap = SsmSnapshot {
290            id,
291            created_at: now,
292            entry_count,
293            total_bytes,
294            delta,
295            checksum,
296            label,
297            state_at_snapshot: current_state,
298        };
299
300        // Evict oldest if at capacity.
301        if self.snapshots.len() >= self.max_snapshots {
302            self.snapshots.pop_front();
303        }
304
305        self.snapshots.push_back(snap);
306        id
307    }
308
309    // -----------------------------------------------------------------------
310    // Snapshot restoration
311    // -----------------------------------------------------------------------
312
313    /// Restore the live state to the point captured by snapshot `id`.
314    ///
315    /// The state stored inline in the snapshot is used directly (O(1) after
316    /// locating the snapshot in the deque).
317    pub fn restore_snapshot(&mut self, id: SnapshotId) -> Result<(), SnapshotError> {
318        let snap = self
319            .snapshots
320            .iter()
321            .find(|s| s.id == id)
322            .ok_or(SnapshotError::SnapshotNotFound(id.0))?;
323
324        self.state.entries = snap.state_at_snapshot.clone();
325        Ok(())
326    }
327
328    // -----------------------------------------------------------------------
329    // Snapshot queries
330    // -----------------------------------------------------------------------
331
332    /// Return all retained snapshots ordered oldest to newest.
333    pub fn list_snapshots(&self) -> Vec<&SsmSnapshot> {
334        self.snapshots.iter().collect()
335    }
336
337    /// Look up a snapshot by id.
338    pub fn get_snapshot(&self, id: SnapshotId) -> Option<&SsmSnapshot> {
339        self.snapshots.iter().find(|s| s.id == id)
340    }
341
342    /// Remove the oldest snapshot from the queue.
343    ///
344    /// Returns `CannotDeleteNonOldest` if `id` does not refer to the oldest
345    /// snapshot, and `SnapshotNotFound` if no snapshot with `id` exists.
346    pub fn delete_snapshot(&mut self, id: SnapshotId) -> Result<(), SnapshotError> {
347        // Must exist.
348        let exists = self.snapshots.iter().any(|s| s.id == id);
349        if !exists {
350            return Err(SnapshotError::SnapshotNotFound(id.0));
351        }
352
353        // Only allow deleting the oldest.
354        let oldest_id = self
355            .snapshots
356            .front()
357            .map(|s| s.id)
358            .ok_or(SnapshotError::EmptySnapshotChain)?;
359
360        if oldest_id != id {
361            return Err(SnapshotError::CannotDeleteNonOldest);
362        }
363
364        self.snapshots.pop_front();
365        Ok(())
366    }
367
368    /// Compute the full state diff between two snapshots.
369    ///
370    /// Reconstructs both states from the stored inline state, then compares.
371    pub fn diff_snapshots(
372        &self,
373        a: SnapshotId,
374        b: SnapshotId,
375    ) -> Result<SnapshotDelta, SnapshotError> {
376        let snap_a = self
377            .snapshots
378            .iter()
379            .find(|s| s.id == a)
380            .ok_or(SnapshotError::SnapshotNotFound(a.0))?;
381        let snap_b = self
382            .snapshots
383            .iter()
384            .find(|s| s.id == b)
385            .ok_or(SnapshotError::SnapshotNotFound(b.0))?;
386
387        let state_a = reconstructed_state(snap_a);
388        let state_b = reconstructed_state(snap_b);
389
390        Ok(compute_delta(state_a, state_b))
391    }
392
393    // -----------------------------------------------------------------------
394    // Counts and statistics
395    // -----------------------------------------------------------------------
396
397    /// Number of snapshots currently retained.
398    pub fn snapshot_count(&self) -> usize {
399        self.snapshots.len()
400    }
401
402    /// Number of entries in the current live state.
403    pub fn live_entry_count(&self) -> usize {
404        self.state.entries.len()
405    }
406
407    /// Sum of value byte sizes in the current live state.
408    pub fn live_total_bytes(&self) -> u64 {
409        self.state
410            .entries
411            .values()
412            .map(|e| e.value.len() as u64)
413            .sum()
414    }
415
416    /// Return aggregate statistics.
417    pub fn stats(&self) -> SnapshotStats {
418        let oldest_snapshot_id = self.snapshots.front().map(|s| s.id.0);
419        let newest_snapshot_id = self.snapshots.back().map(|s| s.id.0);
420        let total_snapshot_bytes: u64 = self.snapshots.iter().map(|s| s.total_bytes).sum();
421        SnapshotStats {
422            snapshot_count: self.snapshots.len(),
423            oldest_snapshot_id,
424            newest_snapshot_id,
425            total_snapshot_bytes,
426            live_entries: self.live_entry_count(),
427            live_bytes: self.live_total_bytes(),
428        }
429    }
430}
431
432// ---------------------------------------------------------------------------
433// Internal delta helper
434// ---------------------------------------------------------------------------
435
436/// Compute a `SnapshotDelta` describing changes from `prev` to `curr`.
437fn compute_delta(
438    prev: &HashMap<String, SnapshotEntry>,
439    curr: &HashMap<String, SnapshotEntry>,
440) -> SnapshotDelta {
441    let mut added = Vec::new();
442    let mut modified = Vec::new();
443    let mut deleted = Vec::new();
444
445    // Keys in curr that are new or changed.
446    for (key, curr_entry) in curr {
447        match prev.get(key) {
448            None => added.push(curr_entry.clone()),
449            Some(prev_entry) => {
450                if prev_entry.version != curr_entry.version || prev_entry.value != curr_entry.value
451                {
452                    modified.push(curr_entry.clone());
453                }
454            }
455        }
456    }
457
458    // Keys in prev that are absent in curr.
459    for key in prev.keys() {
460        if !curr.contains_key(key) {
461            deleted.push(key.clone());
462        }
463    }
464
465    SnapshotDelta {
466        added,
467        modified,
468        deleted,
469    }
470}
471
472// ===========================================================================
473// Legacy block-snapshot API — kept for backwards compatibility
474// ===========================================================================
475
476// ---------------------------------------------------------------------------
477// SnapshotKind
478// ---------------------------------------------------------------------------
479
480/// Describes which blocks are recorded in a legacy snapshot.
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub enum SnapshotKind {
483    /// A complete snapshot of every known block.
484    Full,
485    /// Only the blocks that changed since the immediately preceding snapshot.
486    Incremental,
487    /// All blocks that changed since the most recent `Full` snapshot.
488    Differential,
489}
490
491// ---------------------------------------------------------------------------
492// LegacySnapshotEntry
493// ---------------------------------------------------------------------------
494
495/// A single content-addressed block reference stored inside a legacy snapshot.
496#[derive(Clone, Debug, PartialEq)]
497pub struct LegacySnapshotEntry {
498    /// Content identifier of the block (e.g. a CIDv1 string).
499    pub cid: String,
500    /// Raw byte size of the block.
501    pub size_bytes: u64,
502    /// FNV-1a (64-bit) hash of `cid`, used for fast deduplication lookups.
503    pub hash: u64,
504}
505
506impl LegacySnapshotEntry {
507    /// Construct a new entry, computing the FNV-1a hash automatically.
508    pub fn new(cid: impl Into<String>, size_bytes: u64) -> Self {
509        let cid = cid.into();
510        let hash = fnv1a_64(cid.as_bytes());
511        Self {
512            cid,
513            size_bytes,
514            hash,
515        }
516    }
517}
518
519// ---------------------------------------------------------------------------
520// FNV-1a helpers (public)
521// ---------------------------------------------------------------------------
522
523/// FNV-1a 64-bit hash of an arbitrary byte slice.
524#[inline]
525pub fn fnv1a_64(data: &[u8]) -> u64 {
526    const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
527    const PRIME: u64 = 1_099_511_628_211;
528    let mut hash = OFFSET_BASIS;
529    for &byte in data {
530        hash ^= u64::from(byte);
531        hash = hash.wrapping_mul(PRIME);
532    }
533    hash
534}
535
536// ---------------------------------------------------------------------------
537// LegacySnapshot
538// ---------------------------------------------------------------------------
539
540/// An immutable point-in-time view of a set of storage blocks (legacy API).
541#[derive(Clone, Debug)]
542pub struct LegacySnapshot {
543    /// Unique, monotonically-increasing identifier assigned by the manager.
544    pub snapshot_id: u64,
545    /// What subset of blocks this snapshot captures.
546    pub kind: SnapshotKind,
547    /// Unix timestamp (seconds since epoch) at which this snapshot was taken.
548    pub created_at_secs: u64,
549    /// The block entries recorded in this snapshot.
550    pub entries: Vec<LegacySnapshotEntry>,
551    /// For `Incremental` and `Differential` snapshots, the id of the parent
552    /// snapshot this one is relative to.  Always `None` for `Full` snapshots.
553    pub parent_id: Option<u64>,
554}
555
556impl LegacySnapshot {
557    /// Sum of `size_bytes` across all entries.
558    pub fn total_size(&self) -> u64 {
559        self.entries.iter().map(|e| e.size_bytes).sum()
560    }
561
562    /// Number of block entries in this snapshot.
563    pub fn entry_count(&self) -> usize {
564        self.entries.len()
565    }
566}
567
568// ---------------------------------------------------------------------------
569// SnapshotState
570// ---------------------------------------------------------------------------
571
572/// Lifecycle state of a managed storage snapshot.
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub enum SnapshotState {
575    /// Snapshot is being assembled (not yet queryable).
576    Creating,
577    /// Snapshot is complete and ready for restore / diff.
578    Ready,
579    /// Snapshot exceeded its TTL and is no longer restorable.
580    Expired,
581    /// Snapshot has been explicitly deleted by the user.
582    Deleted,
583}
584
585// ---------------------------------------------------------------------------
586// StorageSnapshot
587// ---------------------------------------------------------------------------
588
589/// A managed, stateful snapshot of storage blocks.
590#[derive(Debug, Clone)]
591pub struct StorageSnapshot {
592    /// Unique id assigned by the manager.
593    pub id: u64,
594    /// Human-readable label for this snapshot.
595    pub label: String,
596    /// Current lifecycle state.
597    pub state: SnapshotState,
598    /// Tick at which this snapshot was created.
599    pub created_tick: u64,
600    /// Number of blocks in this snapshot.
601    pub block_count: usize,
602    /// Total byte size of all blocks.
603    pub total_bytes: u64,
604    /// CIDs of the blocks captured in this snapshot.
605    pub block_cids: Vec<String>,
606}
607
608// ---------------------------------------------------------------------------
609// SnapshotConfig
610// ---------------------------------------------------------------------------
611
612/// Configuration for `LegacyStorageSnapshotManager`.
613#[derive(Debug, Clone)]
614pub struct SnapshotConfig {
615    /// Maximum number of non-deleted snapshots allowed at once.
616    pub max_snapshots: usize,
617    /// How many ticks a snapshot survives before being expired.
618    pub ttl_ticks: u64,
619    /// Whether `tick_cleanup` should automatically remove `Deleted` snapshots.
620    pub auto_cleanup: bool,
621}
622
623impl Default for SnapshotConfig {
624    fn default() -> Self {
625        Self {
626            max_snapshots: 10,
627            ttl_ticks: 1000,
628            auto_cleanup: true,
629        }
630    }
631}
632
633// ---------------------------------------------------------------------------
634// LegacySnapshotDiff
635// ---------------------------------------------------------------------------
636
637/// The difference between two legacy snapshots.
638#[derive(Clone, Debug, Default)]
639pub struct LegacySnapshotDiff {
640    /// CIDs present in snapshot B but absent from snapshot A.
641    pub added: Vec<String>,
642    /// CIDs present in snapshot A but absent from snapshot B.
643    pub removed: Vec<String>,
644    /// Number of CIDs common to both snapshots.
645    pub common: usize,
646}
647
648impl LegacySnapshotDiff {
649    /// Returns `true` when no blocks were added or removed.
650    pub fn is_empty(&self) -> bool {
651        self.added.is_empty() && self.removed.is_empty()
652    }
653}
654
655// ---------------------------------------------------------------------------
656// SnapshotManagerStats
657// ---------------------------------------------------------------------------
658
659/// Aggregate statistics reported by `LegacyStorageSnapshotManager::stats`.
660#[derive(Clone, Debug, Default)]
661pub struct SnapshotManagerStats {
662    /// Total number of snapshots currently held by the manager.
663    pub total_snapshots: usize,
664    /// Number of snapshots in `Ready` state.
665    pub ready_count: usize,
666    /// Number of snapshots in `Expired` state.
667    pub expired_count: usize,
668    /// Lifetime count of snapshots created.
669    pub total_created: u64,
670    /// Lifetime count of snapshots deleted.
671    pub total_deleted: u64,
672}
673
674// ---------------------------------------------------------------------------
675// LegacyStorageSnapshotManager
676// ---------------------------------------------------------------------------
677
678/// Legacy snapshot manager (kept for backwards compatibility).
679///
680/// Manages point-in-time storage snapshots with TTL, lifecycle states,
681/// auto-cleanup, and diff computation.
682pub struct LegacyStorageSnapshotManager {
683    config: SnapshotConfig,
684    snapshots: HashMap<u64, StorageSnapshot>,
685    next_id: u64,
686    current_tick: u64,
687    total_created: u64,
688    total_deleted: u64,
689}
690
691impl LegacyStorageSnapshotManager {
692    /// Create a new manager with the given configuration.
693    pub fn new(config: SnapshotConfig) -> Self {
694        Self {
695            config,
696            snapshots: HashMap::new(),
697            next_id: 1,
698            current_tick: 0,
699            total_created: 0,
700            total_deleted: 0,
701        }
702    }
703
704    /// Create a snapshot with the given label and block data.
705    ///
706    /// Returns the assigned snapshot id on success.
707    /// Returns an error if the maximum number of active (non-deleted)
708    /// snapshots has been reached.
709    pub fn create_snapshot(
710        &mut self,
711        label: &str,
712        block_cids: Vec<String>,
713        total_bytes: u64,
714    ) -> Result<u64, String> {
715        let active_count = self
716            .snapshots
717            .values()
718            .filter(|s| s.state != SnapshotState::Deleted)
719            .count();
720        if active_count >= self.config.max_snapshots {
721            return Err(format!(
722                "max snapshots reached ({})",
723                self.config.max_snapshots
724            ));
725        }
726
727        let id = self.next_id;
728        self.next_id += 1;
729        self.total_created += 1;
730
731        let block_count = block_cids.len();
732        let snapshot = StorageSnapshot {
733            id,
734            label: label.to_owned(),
735            state: SnapshotState::Ready,
736            created_tick: self.current_tick,
737            block_count,
738            total_bytes,
739            block_cids,
740        };
741        self.snapshots.insert(id, snapshot);
742        Ok(id)
743    }
744
745    /// Look up a snapshot by id.
746    pub fn get_snapshot(&self, id: u64) -> Option<&StorageSnapshot> {
747        self.snapshots.get(&id)
748    }
749
750    /// Mark a snapshot as `Deleted`.
751    pub fn delete_snapshot(&mut self, id: u64) -> Result<(), String> {
752        let snap = self
753            .snapshots
754            .get_mut(&id)
755            .ok_or_else(|| format!("snapshot {} not found", id))?;
756        if snap.state == SnapshotState::Deleted {
757            return Err(format!("snapshot {} already deleted", id));
758        }
759        snap.state = SnapshotState::Deleted;
760        self.total_deleted += 1;
761        Ok(())
762    }
763
764    /// Restore a snapshot, returning a clone of its block CIDs.
765    pub fn restore_snapshot(&self, id: u64) -> Result<Vec<String>, String> {
766        let snap = self
767            .snapshots
768            .get(&id)
769            .ok_or_else(|| format!("snapshot {} not found", id))?;
770        match snap.state {
771            SnapshotState::Ready => Ok(snap.block_cids.clone()),
772            SnapshotState::Expired => Err(format!("snapshot {} is expired", id)),
773            SnapshotState::Deleted => Err(format!("snapshot {} is deleted", id)),
774            SnapshotState::Creating => Err(format!("snapshot {} is still being created", id)),
775        }
776    }
777
778    /// List all snapshots as `(id, label, state)` tuples.
779    pub fn list_snapshots(&self) -> Vec<(u64, String, SnapshotState)> {
780        let mut list: Vec<(u64, String, SnapshotState)> = self
781            .snapshots
782            .values()
783            .map(|s| (s.id, s.label.clone(), s.state))
784            .collect();
785        list.sort_by_key(|(id, _, _)| *id);
786        list
787    }
788
789    /// Advance the internal tick, expire TTL-exceeded snapshots, and optionally
790    /// remove `Deleted` snapshots if `auto_cleanup` is enabled.
791    pub fn tick_cleanup(&mut self) {
792        self.current_tick += 1;
793
794        let ttl = self.config.ttl_ticks;
795        let current = self.current_tick;
796
797        for snap in self.snapshots.values_mut() {
798            if snap.state == SnapshotState::Ready && current.saturating_sub(snap.created_tick) > ttl
799            {
800                snap.state = SnapshotState::Expired;
801            }
802        }
803
804        if self.config.auto_cleanup {
805            self.snapshots
806                .retain(|_, s| s.state != SnapshotState::Deleted);
807        }
808    }
809
810    /// Count snapshots currently in `Ready` state.
811    pub fn ready_count(&self) -> usize {
812        self.snapshots
813            .values()
814            .filter(|s| s.state == SnapshotState::Ready)
815            .count()
816    }
817
818    /// Compute the diff between two snapshots identified by their ids.
819    pub fn diff_snapshots(&self, id_a: u64, id_b: u64) -> Result<LegacySnapshotDiff, String> {
820        use std::collections::HashSet;
821
822        let snap_a = self
823            .snapshots
824            .get(&id_a)
825            .ok_or_else(|| format!("snapshot {} not found", id_a))?;
826        let snap_b = self
827            .snapshots
828            .get(&id_b)
829            .ok_or_else(|| format!("snapshot {} not found", id_b))?;
830
831        let set_a: HashSet<&str> = snap_a.block_cids.iter().map(|s| s.as_str()).collect();
832        let set_b: HashSet<&str> = snap_b.block_cids.iter().map(|s| s.as_str()).collect();
833
834        let mut added: Vec<String> = set_b.difference(&set_a).map(|s| (*s).to_owned()).collect();
835        let mut removed: Vec<String> = set_a.difference(&set_b).map(|s| (*s).to_owned()).collect();
836        added.sort_unstable();
837        removed.sort_unstable();
838        let common = set_a.intersection(&set_b).count();
839
840        Ok(LegacySnapshotDiff {
841            added,
842            removed,
843            common,
844        })
845    }
846
847    /// Return aggregate statistics.
848    pub fn stats(&self) -> SnapshotManagerStats {
849        let mut ready_count = 0usize;
850        let mut expired_count = 0usize;
851        for snap in self.snapshots.values() {
852            match snap.state {
853                SnapshotState::Ready => ready_count += 1,
854                SnapshotState::Expired => expired_count += 1,
855                _ => {}
856            }
857        }
858        SnapshotManagerStats {
859            total_snapshots: self.snapshots.len(),
860            ready_count,
861            expired_count,
862            total_created: self.total_created,
863            total_deleted: self.total_deleted,
864        }
865    }
866}
867
868impl Default for LegacyStorageSnapshotManager {
869    fn default() -> Self {
870        Self::new(SnapshotConfig::default())
871    }
872}
873
874// ===========================================================================
875// Tests
876// ===========================================================================
877
878#[cfg(test)]
879mod tests {
880    use std::collections::HashMap;
881
882    use crate::snapshot_manager::SnapshotEntry;
883    use crate::snapshot_manager::{
884        compute_checksum, fnv1a_64, LegacySnapshotDiff, LegacyStorageSnapshotManager,
885        SnapshotConfig, SnapshotError, SnapshotId, SnapshotState, StorageSnapshotManager,
886    };
887
888    // -----------------------------------------------------------------------
889    // Helpers
890    // -----------------------------------------------------------------------
891
892    fn build_mgr(max: usize) -> StorageSnapshotManager {
893        StorageSnapshotManager::new(max)
894    }
895
896    // -----------------------------------------------------------------------
897    // 1. New empty manager has zero entries and zero snapshots
898    // -----------------------------------------------------------------------
899    #[test]
900    fn test_new_manager_is_empty() {
901        let mgr = build_mgr(10);
902        assert_eq!(mgr.live_entry_count(), 0);
903        assert_eq!(mgr.live_total_bytes(), 0);
904        assert_eq!(mgr.snapshot_count(), 0);
905        assert!(mgr.list_snapshots().is_empty());
906    }
907
908    // -----------------------------------------------------------------------
909    // 2. put inserts an entry and get retrieves it
910    // -----------------------------------------------------------------------
911    #[test]
912    fn test_put_and_get() {
913        let mut mgr = build_mgr(10);
914        mgr.put("alpha".into(), b"hello".to_vec(), 1);
915        assert_eq!(mgr.get("alpha"), Some(b"hello".as_slice()));
916    }
917
918    // -----------------------------------------------------------------------
919    // 3. get returns None for unknown key
920    // -----------------------------------------------------------------------
921    #[test]
922    fn test_get_missing_key() {
923        let mgr = build_mgr(10);
924        assert!(mgr.get("nope").is_none());
925    }
926
927    // -----------------------------------------------------------------------
928    // 4. delete removes an existing key
929    // -----------------------------------------------------------------------
930    #[test]
931    fn test_delete_existing_key() {
932        let mut mgr = build_mgr(10);
933        mgr.put("k".into(), b"v".to_vec(), 0);
934        let removed = mgr.delete("k");
935        assert!(removed);
936        assert!(mgr.get("k").is_none());
937    }
938
939    // -----------------------------------------------------------------------
940    // 5. delete returns false for non-existent key
941    // -----------------------------------------------------------------------
942    #[test]
943    fn test_delete_missing_key() {
944        let mut mgr = build_mgr(10);
945        assert!(!mgr.delete("ghost"));
946    }
947
948    // -----------------------------------------------------------------------
949    // 6. live_entry_count tracks additions and deletions
950    // -----------------------------------------------------------------------
951    #[test]
952    fn test_live_entry_count() {
953        let mut mgr = build_mgr(10);
954        assert_eq!(mgr.live_entry_count(), 0);
955        mgr.put("a".into(), b"1".to_vec(), 0);
956        mgr.put("b".into(), b"2".to_vec(), 0);
957        assert_eq!(mgr.live_entry_count(), 2);
958        mgr.delete("a");
959        assert_eq!(mgr.live_entry_count(), 1);
960    }
961
962    // -----------------------------------------------------------------------
963    // 7. live_total_bytes is sum of value lengths
964    // -----------------------------------------------------------------------
965    #[test]
966    fn test_live_total_bytes() {
967        let mut mgr = build_mgr(10);
968        mgr.put("x".into(), vec![0u8; 10], 0);
969        mgr.put("y".into(), vec![0u8; 20], 0);
970        assert_eq!(mgr.live_total_bytes(), 30);
971    }
972
973    // -----------------------------------------------------------------------
974    // 8. take_snapshot on empty state produces full snapshot with no delta
975    // -----------------------------------------------------------------------
976    #[test]
977    fn test_first_snapshot_is_full() {
978        let mut mgr = build_mgr(10);
979        let id = mgr.take_snapshot(None, 100);
980        let snap = mgr.get_snapshot(id).expect("must exist");
981        assert!(
982            snap.delta.is_none(),
983            "first snapshot must be full (no delta)"
984        );
985    }
986
987    // -----------------------------------------------------------------------
988    // 9. take_snapshot returns monotonically increasing ids
989    // -----------------------------------------------------------------------
990    #[test]
991    fn test_snapshot_ids_monotonic() {
992        let mut mgr = build_mgr(10);
993        let id1 = mgr.take_snapshot(None, 0);
994        let id2 = mgr.take_snapshot(None, 1);
995        let id3 = mgr.take_snapshot(None, 2);
996        assert!(id1 < id2);
997        assert!(id2 < id3);
998    }
999
1000    // -----------------------------------------------------------------------
1001    // 10. take_snapshot captures correct entry_count and total_bytes
1002    // -----------------------------------------------------------------------
1003    #[test]
1004    fn test_snapshot_entry_count_and_bytes() {
1005        let mut mgr = build_mgr(10);
1006        mgr.put("k1".into(), vec![1u8; 8], 0);
1007        mgr.put("k2".into(), vec![2u8; 16], 0);
1008        let id = mgr.take_snapshot(None, 0);
1009        let snap = mgr.get_snapshot(id).expect("must exist");
1010        assert_eq!(snap.entry_count, 2);
1011        assert_eq!(snap.total_bytes, 24);
1012    }
1013
1014    // -----------------------------------------------------------------------
1015    // 11. take_snapshot captures label
1016    // -----------------------------------------------------------------------
1017    #[test]
1018    fn test_snapshot_label() {
1019        let mut mgr = build_mgr(10);
1020        let id = mgr.take_snapshot(Some("my-label".to_owned()), 0);
1021        let snap = mgr.get_snapshot(id).expect("must exist");
1022        assert_eq!(snap.label.as_deref(), Some("my-label"));
1023    }
1024
1025    // -----------------------------------------------------------------------
1026    // 12. Second snapshot has a delta with added entries
1027    // -----------------------------------------------------------------------
1028    #[test]
1029    fn test_second_snapshot_delta_added() {
1030        let mut mgr = build_mgr(10);
1031        mgr.take_snapshot(None, 0); // full (empty)
1032        mgr.put("new".into(), b"data".to_vec(), 1);
1033        let id2 = mgr.take_snapshot(None, 2);
1034        let snap2 = mgr.get_snapshot(id2).expect("must exist");
1035        let delta = snap2
1036            .delta
1037            .as_ref()
1038            .expect("second snapshot must have delta");
1039        assert_eq!(delta.added.len(), 1);
1040        assert_eq!(delta.added[0].key, "new");
1041        assert!(delta.modified.is_empty());
1042        assert!(delta.deleted.is_empty());
1043    }
1044
1045    // -----------------------------------------------------------------------
1046    // 13. Delta captures modified entries
1047    // -----------------------------------------------------------------------
1048    #[test]
1049    fn test_snapshot_delta_modified() {
1050        let mut mgr = build_mgr(10);
1051        mgr.put("k".into(), b"v1".to_vec(), 0);
1052        mgr.take_snapshot(None, 0);
1053        mgr.put("k".into(), b"v2".to_vec(), 1);
1054        let id2 = mgr.take_snapshot(None, 1);
1055        let snap2 = mgr.get_snapshot(id2).expect("must exist");
1056        let delta = snap2.delta.as_ref().expect("must have delta");
1057        assert!(delta.added.is_empty());
1058        assert_eq!(delta.modified.len(), 1);
1059        assert_eq!(delta.modified[0].key, "k");
1060        assert_eq!(delta.modified[0].value, b"v2");
1061        assert!(delta.deleted.is_empty());
1062    }
1063
1064    // -----------------------------------------------------------------------
1065    // 14. Delta captures deleted entries
1066    // -----------------------------------------------------------------------
1067    #[test]
1068    fn test_snapshot_delta_deleted() {
1069        let mut mgr = build_mgr(10);
1070        mgr.put("gone".into(), b"bye".to_vec(), 0);
1071        mgr.take_snapshot(None, 0);
1072        mgr.delete("gone");
1073        let id2 = mgr.take_snapshot(None, 1);
1074        let snap2 = mgr.get_snapshot(id2).expect("must exist");
1075        let delta = snap2.delta.as_ref().expect("must have delta");
1076        assert!(delta.added.is_empty());
1077        assert!(delta.modified.is_empty());
1078        assert_eq!(delta.deleted, vec!["gone".to_owned()]);
1079    }
1080
1081    // -----------------------------------------------------------------------
1082    // 15. restore_snapshot rebuilds live state
1083    // -----------------------------------------------------------------------
1084    #[test]
1085    fn test_restore_snapshot_basic() {
1086        let mut mgr = build_mgr(10);
1087        mgr.put("a".into(), b"original".to_vec(), 0);
1088        let id = mgr.take_snapshot(None, 0);
1089
1090        // Mutate live state.
1091        mgr.put("a".into(), b"changed".to_vec(), 1);
1092        mgr.put("b".into(), b"new".to_vec(), 1);
1093        assert_eq!(mgr.get("a"), Some(b"changed".as_slice()));
1094
1095        // Restore to snapshot.
1096        mgr.restore_snapshot(id).expect("restore should succeed");
1097        assert_eq!(mgr.get("a"), Some(b"original".as_slice()));
1098        assert!(mgr.get("b").is_none(), "b was not in snapshot");
1099    }
1100
1101    // -----------------------------------------------------------------------
1102    // 16. restore_snapshot returns SnapshotNotFound for unknown id
1103    // -----------------------------------------------------------------------
1104    #[test]
1105    fn test_restore_snapshot_not_found() {
1106        let mut mgr = build_mgr(10);
1107        let result = mgr.restore_snapshot(SnapshotId(999));
1108        assert_eq!(result, Err(SnapshotError::SnapshotNotFound(999)));
1109    }
1110
1111    // -----------------------------------------------------------------------
1112    // 17. get_snapshot returns None for unknown id
1113    // -----------------------------------------------------------------------
1114    #[test]
1115    fn test_get_snapshot_unknown_id() {
1116        let mgr = build_mgr(10);
1117        assert!(mgr.get_snapshot(SnapshotId(42)).is_none());
1118    }
1119
1120    // -----------------------------------------------------------------------
1121    // 18. list_snapshots returns ordered oldest-to-newest
1122    // -----------------------------------------------------------------------
1123    #[test]
1124    fn test_list_snapshots_order() {
1125        let mut mgr = build_mgr(10);
1126        let id1 = mgr.take_snapshot(None, 0);
1127        let id2 = mgr.take_snapshot(None, 1);
1128        let id3 = mgr.take_snapshot(None, 2);
1129        let list = mgr.list_snapshots();
1130        assert_eq!(list.len(), 3);
1131        assert_eq!(list[0].id, id1);
1132        assert_eq!(list[1].id, id2);
1133        assert_eq!(list[2].id, id3);
1134    }
1135
1136    // -----------------------------------------------------------------------
1137    // 19. Oldest snapshot is evicted when max_snapshots is exceeded
1138    // -----------------------------------------------------------------------
1139    #[test]
1140    fn test_max_snapshots_eviction() {
1141        let mut mgr = build_mgr(3);
1142        let id1 = mgr.take_snapshot(None, 0);
1143        let id2 = mgr.take_snapshot(None, 1);
1144        let id3 = mgr.take_snapshot(None, 2);
1145        // id1 should still be present before overflow.
1146        assert!(mgr.get_snapshot(id1).is_some());
1147        // Add a 4th — id1 should be evicted.
1148        let id4 = mgr.take_snapshot(None, 3);
1149        assert!(mgr.get_snapshot(id1).is_none(), "id1 must be evicted");
1150        assert!(mgr.get_snapshot(id2).is_some());
1151        assert!(mgr.get_snapshot(id3).is_some());
1152        assert!(mgr.get_snapshot(id4).is_some());
1153        assert_eq!(mgr.snapshot_count(), 3);
1154    }
1155
1156    // -----------------------------------------------------------------------
1157    // 20. delete_snapshot removes the oldest snapshot
1158    // -----------------------------------------------------------------------
1159    #[test]
1160    fn test_delete_oldest_snapshot() {
1161        let mut mgr = build_mgr(10);
1162        let id1 = mgr.take_snapshot(None, 0);
1163        let _id2 = mgr.take_snapshot(None, 1);
1164        mgr.delete_snapshot(id1)
1165            .expect("delete oldest should succeed");
1166        assert!(mgr.get_snapshot(id1).is_none());
1167        assert_eq!(mgr.snapshot_count(), 1);
1168    }
1169
1170    // -----------------------------------------------------------------------
1171    // 21. delete_snapshot CannotDeleteNonOldest for non-oldest id
1172    // -----------------------------------------------------------------------
1173    #[test]
1174    fn test_delete_non_oldest_snapshot_errors() {
1175        let mut mgr = build_mgr(10);
1176        let _id1 = mgr.take_snapshot(None, 0);
1177        let id2 = mgr.take_snapshot(None, 1);
1178        let result = mgr.delete_snapshot(id2);
1179        assert_eq!(result, Err(SnapshotError::CannotDeleteNonOldest));
1180    }
1181
1182    // -----------------------------------------------------------------------
1183    // 22. delete_snapshot SnapshotNotFound for unknown id
1184    // -----------------------------------------------------------------------
1185    #[test]
1186    fn test_delete_snapshot_not_found() {
1187        let mut mgr = build_mgr(10);
1188        let result = mgr.delete_snapshot(SnapshotId(404));
1189        assert_eq!(result, Err(SnapshotError::SnapshotNotFound(404)));
1190    }
1191
1192    // -----------------------------------------------------------------------
1193    // 23. diff_snapshots correctly identifies changes between two snapshots
1194    // -----------------------------------------------------------------------
1195    #[test]
1196    fn test_diff_snapshots() {
1197        let mut mgr = build_mgr(10);
1198        mgr.put("common".into(), b"same".to_vec(), 0);
1199        mgr.put("will-change".into(), b"old".to_vec(), 0);
1200        mgr.put("will-delete".into(), b"bye".to_vec(), 0);
1201        let id_a = mgr.take_snapshot(None, 0);
1202
1203        mgr.put("will-change".into(), b"new".to_vec(), 1);
1204        mgr.delete("will-delete");
1205        mgr.put("added".into(), b"fresh".to_vec(), 1);
1206        let id_b = mgr.take_snapshot(None, 1);
1207
1208        let delta = mgr.diff_snapshots(id_a, id_b).expect("diff should succeed");
1209        assert_eq!(delta.added.len(), 1);
1210        assert_eq!(delta.added[0].key, "added");
1211        assert_eq!(delta.modified.len(), 1);
1212        assert_eq!(delta.modified[0].key, "will-change");
1213        assert_eq!(delta.deleted, vec!["will-delete".to_owned()]);
1214    }
1215
1216    // -----------------------------------------------------------------------
1217    // 24. diff_snapshots returns SnapshotNotFound for bad ids
1218    // -----------------------------------------------------------------------
1219    #[test]
1220    fn test_diff_snapshots_not_found() {
1221        let mut mgr = build_mgr(10);
1222        let id = mgr.take_snapshot(None, 0);
1223        assert_eq!(
1224            mgr.diff_snapshots(id, SnapshotId(999)),
1225            Err(SnapshotError::SnapshotNotFound(999))
1226        );
1227        assert_eq!(
1228            mgr.diff_snapshots(SnapshotId(999), id),
1229            Err(SnapshotError::SnapshotNotFound(999))
1230        );
1231    }
1232
1233    // -----------------------------------------------------------------------
1234    // 25. stats() reflects accurate counts
1235    // -----------------------------------------------------------------------
1236    #[test]
1237    fn test_stats_accuracy() {
1238        let mut mgr = build_mgr(10);
1239        mgr.put("a".into(), vec![0u8; 5], 0);
1240        mgr.put("b".into(), vec![0u8; 10], 0);
1241        let id1 = mgr.take_snapshot(None, 0);
1242        let _id2 = mgr.take_snapshot(None, 1);
1243
1244        let stats = mgr.stats();
1245        assert_eq!(stats.snapshot_count, 2);
1246        assert_eq!(stats.oldest_snapshot_id, Some(id1.0));
1247        assert_eq!(stats.live_entries, 2);
1248        assert_eq!(stats.live_bytes, 15);
1249    }
1250
1251    // -----------------------------------------------------------------------
1252    // 26. stats() on empty manager
1253    // -----------------------------------------------------------------------
1254    #[test]
1255    fn test_stats_empty() {
1256        let mgr = build_mgr(10);
1257        let stats = mgr.stats();
1258        assert_eq!(stats.snapshot_count, 0);
1259        assert!(stats.oldest_snapshot_id.is_none());
1260        assert!(stats.newest_snapshot_id.is_none());
1261        assert_eq!(stats.total_snapshot_bytes, 0);
1262        assert_eq!(stats.live_entries, 0);
1263        assert_eq!(stats.live_bytes, 0);
1264    }
1265
1266    // -----------------------------------------------------------------------
1267    // 27. checksum is deterministic
1268    // -----------------------------------------------------------------------
1269    #[test]
1270    fn test_checksum_deterministic() {
1271        let mut map1 = HashMap::new();
1272        map1.insert(
1273            "k1".to_owned(),
1274            SnapshotEntry {
1275                key: "k1".into(),
1276                value: b"val1".to_vec(),
1277                version: 1,
1278            },
1279        );
1280        map1.insert(
1281            "k2".to_owned(),
1282            SnapshotEntry {
1283                key: "k2".into(),
1284                value: b"val2".to_vec(),
1285                version: 2,
1286            },
1287        );
1288
1289        let mut map2 = HashMap::new();
1290        map2.insert(
1291            "k2".to_owned(),
1292            SnapshotEntry {
1293                key: "k2".into(),
1294                value: b"val2".to_vec(),
1295                version: 2,
1296            },
1297        );
1298        map2.insert(
1299            "k1".to_owned(),
1300            SnapshotEntry {
1301                key: "k1".into(),
1302                value: b"val1".to_vec(),
1303                version: 1,
1304            },
1305        );
1306
1307        assert_eq!(
1308            compute_checksum(&map1),
1309            compute_checksum(&map2),
1310            "checksum must be order-independent"
1311        );
1312    }
1313
1314    // -----------------------------------------------------------------------
1315    // 28. checksum changes when value changes
1316    // -----------------------------------------------------------------------
1317    #[test]
1318    fn test_checksum_changes_on_mutation() {
1319        let mut map1 = HashMap::new();
1320        map1.insert(
1321            "k".to_owned(),
1322            SnapshotEntry {
1323                key: "k".into(),
1324                value: b"original".to_vec(),
1325                version: 1,
1326            },
1327        );
1328        let c1 = compute_checksum(&map1);
1329
1330        let mut map2 = HashMap::new();
1331        map2.insert(
1332            "k".to_owned(),
1333            SnapshotEntry {
1334                key: "k".into(),
1335                value: b"changed!".to_vec(),
1336                version: 2,
1337            },
1338        );
1339        let c2 = compute_checksum(&map2);
1340
1341        assert_ne!(c1, c2);
1342    }
1343
1344    // -----------------------------------------------------------------------
1345    // 29. empty delta is_empty() returns true
1346    // -----------------------------------------------------------------------
1347    #[test]
1348    fn test_snapshot_delta_is_empty() {
1349        let mut mgr = build_mgr(10);
1350        mgr.put("x".into(), b"same".to_vec(), 0);
1351        let id1 = mgr.take_snapshot(None, 0);
1352        // No changes between id1 and id2.
1353        let id2 = mgr.take_snapshot(None, 1);
1354        let delta = mgr.diff_snapshots(id1, id2).expect("ok");
1355        assert!(delta.is_empty());
1356    }
1357
1358    // -----------------------------------------------------------------------
1359    // 30. restore to an earlier snapshot then re-snapshot preserves history
1360    // -----------------------------------------------------------------------
1361    #[test]
1362    fn test_restore_and_resnap() {
1363        let mut mgr = build_mgr(10);
1364        mgr.put("key".into(), b"v1".to_vec(), 0);
1365        let id1 = mgr.take_snapshot(Some("snap-1".into()), 0);
1366        mgr.put("key".into(), b"v2".to_vec(), 1);
1367        mgr.take_snapshot(Some("snap-2".into()), 1);
1368
1369        // Restore to snap-1.
1370        mgr.restore_snapshot(id1).expect("restore");
1371        assert_eq!(mgr.get("key"), Some(b"v1".as_slice()));
1372
1373        // Take a new snapshot after restore.
1374        let id3 = mgr.take_snapshot(Some("snap-3".into()), 2);
1375        let snap3 = mgr.get_snapshot(id3).expect("must exist");
1376        // snap3 is relative to snap-2 (the previous head in the deque).
1377        assert_eq!(snap3.entry_count, 1);
1378    }
1379
1380    // -----------------------------------------------------------------------
1381    // 31. Multiple puts to same key preserve latest version
1382    // -----------------------------------------------------------------------
1383    #[test]
1384    fn test_put_overwrites_value() {
1385        let mut mgr = build_mgr(10);
1386        mgr.put("k".into(), b"first".to_vec(), 0);
1387        mgr.put("k".into(), b"second".to_vec(), 0);
1388        mgr.put("k".into(), b"third".to_vec(), 0);
1389        assert_eq!(mgr.get("k"), Some(b"third".as_slice()));
1390        assert_eq!(mgr.live_entry_count(), 1);
1391    }
1392
1393    // -----------------------------------------------------------------------
1394    // 32. snapshot_count returns correct value
1395    // -----------------------------------------------------------------------
1396    #[test]
1397    fn test_snapshot_count() {
1398        let mut mgr = build_mgr(10);
1399        assert_eq!(mgr.snapshot_count(), 0);
1400        mgr.take_snapshot(None, 0);
1401        assert_eq!(mgr.snapshot_count(), 1);
1402        mgr.take_snapshot(None, 1);
1403        assert_eq!(mgr.snapshot_count(), 2);
1404    }
1405
1406    // -----------------------------------------------------------------------
1407    // 33. diff_snapshots on same id returns empty delta
1408    // -----------------------------------------------------------------------
1409    #[test]
1410    fn test_diff_same_snapshot() {
1411        let mut mgr = build_mgr(10);
1412        mgr.put("k".into(), b"v".to_vec(), 0);
1413        let id = mgr.take_snapshot(None, 0);
1414        let delta = mgr.diff_snapshots(id, id).expect("ok");
1415        assert!(delta.is_empty());
1416    }
1417
1418    // -----------------------------------------------------------------------
1419    // 34. stats newest_snapshot_id updated after multiple snapshots
1420    // -----------------------------------------------------------------------
1421    #[test]
1422    fn test_stats_newest_id() {
1423        let mut mgr = build_mgr(10);
1424        mgr.take_snapshot(None, 0);
1425        let id_last = mgr.take_snapshot(None, 1);
1426        let stats = mgr.stats();
1427        assert_eq!(stats.newest_snapshot_id, Some(id_last.0));
1428    }
1429
1430    // -----------------------------------------------------------------------
1431    // 35. snapshot checksum stored in SsmSnapshot
1432    // -----------------------------------------------------------------------
1433    #[test]
1434    fn test_snapshot_checksum_stored() {
1435        let mut mgr = build_mgr(10);
1436        mgr.put("ck".into(), b"data".to_vec(), 0);
1437        let id = mgr.take_snapshot(None, 0);
1438        let snap = mgr.get_snapshot(id).expect("must exist");
1439        assert_ne!(
1440            snap.checksum, 0,
1441            "checksum must be non-zero for non-empty state"
1442        );
1443    }
1444
1445    // -----------------------------------------------------------------------
1446    // 36. Snapshot created_at stores the provided timestamp
1447    // -----------------------------------------------------------------------
1448    #[test]
1449    fn test_snapshot_created_at() {
1450        let mut mgr = build_mgr(10);
1451        let id = mgr.take_snapshot(None, 1234567890);
1452        let snap = mgr.get_snapshot(id).expect("must exist");
1453        assert_eq!(snap.created_at, 1234567890);
1454    }
1455
1456    // -----------------------------------------------------------------------
1457    // Legacy API tests
1458    // -----------------------------------------------------------------------
1459
1460    fn default_legacy_config() -> SnapshotConfig {
1461        SnapshotConfig::default()
1462    }
1463
1464    fn small_legacy_config(max: usize, ttl: u64) -> SnapshotConfig {
1465        SnapshotConfig {
1466            max_snapshots: max,
1467            ttl_ticks: ttl,
1468            auto_cleanup: true,
1469        }
1470    }
1471
1472    // -----------------------------------------------------------------------
1473    // L1. Legacy create snapshot
1474    // -----------------------------------------------------------------------
1475    #[test]
1476    fn test_legacy_create_snapshot_fields() {
1477        let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1478        let cids = vec!["cid_a".to_owned(), "cid_b".to_owned()];
1479        let id = mgr
1480            .create_snapshot("backup-1", cids.clone(), 300)
1481            .expect("should succeed");
1482
1483        let snap = mgr.get_snapshot(id).expect("snapshot must exist");
1484        assert_eq!(snap.id, id);
1485        assert_eq!(snap.label, "backup-1");
1486        assert_eq!(snap.state, SnapshotState::Ready);
1487        assert_eq!(snap.block_count, 2);
1488        assert_eq!(snap.total_bytes, 300);
1489        assert_eq!(snap.block_cids, cids);
1490    }
1491
1492    // -----------------------------------------------------------------------
1493    // L2. Legacy restore
1494    // -----------------------------------------------------------------------
1495    #[test]
1496    fn test_legacy_restore_snapshot() {
1497        let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1498        let cids = vec!["a".into(), "b".into(), "c".into()];
1499        let id = mgr
1500            .create_snapshot("restore-me", cids.clone(), 100)
1501            .expect("ok");
1502        let restored = mgr.restore_snapshot(id).expect("restore should succeed");
1503        assert_eq!(restored, cids);
1504    }
1505
1506    // -----------------------------------------------------------------------
1507    // L3. Legacy diff — added
1508    // -----------------------------------------------------------------------
1509    #[test]
1510    fn test_legacy_diff_snapshots_added() {
1511        let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1512        let id_a = mgr
1513            .create_snapshot("a", vec!["common".into()], 10)
1514            .expect("ok");
1515        let id_b = mgr
1516            .create_snapshot("b", vec!["common".into(), "new".into()], 20)
1517            .expect("ok");
1518        let diff: LegacySnapshotDiff = mgr.diff_snapshots(id_a, id_b).expect("ok");
1519        assert_eq!(diff.added, vec!["new".to_owned()]);
1520        assert!(diff.removed.is_empty());
1521        assert_eq!(diff.common, 1);
1522    }
1523
1524    // -----------------------------------------------------------------------
1525    // L4. Legacy TTL expiration
1526    // -----------------------------------------------------------------------
1527    #[test]
1528    fn test_legacy_ttl_expiration() {
1529        let mut mgr = LegacyStorageSnapshotManager::new(small_legacy_config(10, 2));
1530        let id = mgr.create_snapshot("ttl-test", vec![], 0).expect("ok");
1531        mgr.tick_cleanup();
1532        mgr.tick_cleanup();
1533        assert_eq!(
1534            mgr.get_snapshot(id).expect("exists").state,
1535            SnapshotState::Ready,
1536        );
1537        mgr.tick_cleanup();
1538        assert_eq!(
1539            mgr.get_snapshot(id).expect("exists").state,
1540            SnapshotState::Expired,
1541        );
1542    }
1543
1544    // -----------------------------------------------------------------------
1545    // L5. Legacy max snapshots limit
1546    // -----------------------------------------------------------------------
1547    #[test]
1548    fn test_legacy_max_snapshots() {
1549        let mut mgr = LegacyStorageSnapshotManager::new(small_legacy_config(2, 1000));
1550        mgr.create_snapshot("s1", vec![], 0).expect("ok");
1551        mgr.create_snapshot("s2", vec![], 0).expect("ok");
1552        let result = mgr.create_snapshot("s3", vec![], 0);
1553        assert!(result.is_err());
1554    }
1555
1556    // -----------------------------------------------------------------------
1557    // 37. fnv1a_64 is deterministic
1558    // -----------------------------------------------------------------------
1559    #[test]
1560    fn test_fnv1a_deterministic() {
1561        let h1 = fnv1a_64(b"hello");
1562        let h2 = fnv1a_64(b"hello");
1563        assert_eq!(h1, h2);
1564        assert_ne!(h1, fnv1a_64(b"world"));
1565    }
1566
1567    // -----------------------------------------------------------------------
1568    // 38. Snapshot with label=None stores None
1569    // -----------------------------------------------------------------------
1570    #[test]
1571    fn test_snapshot_no_label() {
1572        let mut mgr = build_mgr(10);
1573        let id = mgr.take_snapshot(None, 0);
1574        let snap = mgr.get_snapshot(id).expect("must exist");
1575        assert!(snap.label.is_none());
1576    }
1577
1578    // -----------------------------------------------------------------------
1579    // 39. delete_snapshot on single-element deque leaves it empty
1580    // -----------------------------------------------------------------------
1581    #[test]
1582    fn test_delete_only_snapshot() {
1583        let mut mgr = build_mgr(10);
1584        let id = mgr.take_snapshot(None, 0);
1585        mgr.delete_snapshot(id).expect("should succeed");
1586        assert_eq!(mgr.snapshot_count(), 0);
1587        // Now trying to delete again fails with NotFound.
1588        let result = mgr.delete_snapshot(id);
1589        assert_eq!(result, Err(SnapshotError::SnapshotNotFound(id.0)));
1590    }
1591
1592    // -----------------------------------------------------------------------
1593    // 40. SnapshotId Display formatting
1594    // -----------------------------------------------------------------------
1595    #[test]
1596    fn test_snapshot_id_display() {
1597        let sid = SnapshotId(42);
1598        assert_eq!(format!("{}", sid), "SnapshotId(42)");
1599    }
1600}