kaccy_reputation/
cache.rs

1//! # Caching Layer for Reputation Data
2//!
3//! Provides high-performance in-memory caching for frequently accessed reputation scores
4//! and tier information to significantly reduce database load.
5//!
6//! ## Overview
7//!
8//! The caching layer uses an in-memory HashMap with TTL (Time-To-Live) support,
9//! thread-safe RwLock for concurrent access, and automatic expiration handling.
10//!
11//! ## Performance Benefits
12//!
13//! - **Reduces database queries** by 80-95% for frequently accessed data
14//! - **O(1) lookups** using HashMap
15//! - **Thread-safe** with RwLock (many readers, single writer)
16//! - **Automatic cleanup** of expired entries
17//!
18//! ## Usage Example
19//!
20//! ```rust
21//! use kaccy_reputation::*;
22//! use rust_decimal_macros::dec;
23//! use uuid::Uuid;
24//!
25//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
26//! // Create cache with 5-minute TTL
27//! let cache = ReputationCache::new(300);
28//! let user_id = Uuid::new_v4();
29//!
30//! // Cache a reputation score
31//! let score = ReputationScore {
32//!     user_id,
33//!     overall_score: dec!(750),
34//!     tier: ReputationTier::Gold,
35//!     components: ScoreComponents::default(),
36//! };
37//! cache.set_score(user_id, score)?;
38//!
39//! // Retrieve from cache (very fast!)
40//! if let Some(cached) = cache.get_score(user_id) {
41//!     println!("Score: {}", cached.overall_score);
42//! }
43//!
44//! // Invalidate on update
45//! cache.invalidate_user(user_id);
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! ## Cache Invalidation Pattern
51//!
52//! ```rust,no_run
53//! # use kaccy_reputation::*;
54//! # use uuid::Uuid;
55//! # use rust_decimal::Decimal;
56//! # async fn example(
57//! #     cache: &ReputationCache,
58//! #     db: &sqlx::PgPool,
59//! #     user_id: Uuid,
60//! #     delta: Decimal
61//! # ) -> Result<(), Box<dyn std::error::Error>> {
62//! // Update database
63//! sqlx::query("UPDATE users SET reputation_score = reputation_score + $1 WHERE user_id = $2")
64//!     .bind(delta)
65//!     .bind(user_id)
66//!     .execute(db)
67//!     .await?;
68//!
69//! // Invalidate cache to force refresh on next read
70//! cache.invalidate_user(user_id);
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! ## Best Practices
76//!
77//! - **Set appropriate TTL**: 60-300 seconds for high-frequency updates, 300-900 for moderate
78//! - **Always invalidate** when data changes in the database
79//! - **Monitor hit rates** using `get_stats()` to tune TTL
80//! - **Run cleanup** periodically (e.g., hourly) with `cleanup_expired()`
81//! - **Don't cache** rarely accessed data (wastes memory)
82//!
83//! ## Thread Safety
84//!
85//! All cache operations are thread-safe using `RwLock`:
86//! - Multiple threads can read simultaneously
87//! - Writes block all access but are fast (just HashMap insert)
88
89use chrono::{DateTime, Utc};
90use rust_decimal::Decimal;
91use std::collections::HashMap;
92use std::sync::{Arc, RwLock};
93use uuid::Uuid;
94
95use crate::error::ReputationError;
96use crate::score::ReputationScore;
97use crate::tier::ReputationTier;
98
99/// TTL for cache entries in seconds
100const DEFAULT_TTL_SECONDS: i64 = 300; // 5 minutes
101
102/// Cached reputation score entry
103#[derive(Debug, Clone)]
104pub struct CachedScore {
105    pub score: ReputationScore,
106    pub cached_at: DateTime<Utc>,
107    pub ttl_seconds: i64,
108}
109
110impl CachedScore {
111    /// Check if cache entry is still valid
112    pub fn is_valid(&self) -> bool {
113        let now = Utc::now();
114        let age = now.signed_duration_since(self.cached_at).num_seconds();
115        age < self.ttl_seconds
116    }
117
118    /// Get remaining TTL in seconds
119    pub fn remaining_ttl(&self) -> i64 {
120        let now = Utc::now();
121        let age = now.signed_duration_since(self.cached_at).num_seconds();
122        (self.ttl_seconds - age).max(0)
123    }
124}
125
126/// Cached tier information
127#[derive(Debug, Clone)]
128pub struct CachedTier {
129    pub tier: ReputationTier,
130    pub score: Decimal,
131    pub cached_at: DateTime<Utc>,
132}
133
134/// Cache hit rate metrics
135#[derive(Debug, Clone, Default)]
136struct HitRateMetrics {
137    score_hits: usize,
138    score_misses: usize,
139    tier_hits: usize,
140    tier_misses: usize,
141}
142
143/// In-memory cache for reputation data
144#[derive(Debug, Clone)]
145pub struct ReputationCache {
146    scores: Arc<RwLock<HashMap<Uuid, CachedScore>>>,
147    tiers: Arc<RwLock<HashMap<Uuid, CachedTier>>>,
148    ttl_seconds: i64,
149    metrics: Arc<RwLock<HitRateMetrics>>,
150}
151
152impl Default for ReputationCache {
153    fn default() -> Self {
154        Self::new(DEFAULT_TTL_SECONDS)
155    }
156}
157
158impl ReputationCache {
159    /// Create a new reputation cache with specified TTL
160    pub fn new(ttl_seconds: i64) -> Self {
161        Self {
162            scores: Arc::new(RwLock::new(HashMap::new())),
163            tiers: Arc::new(RwLock::new(HashMap::new())),
164            ttl_seconds,
165            metrics: Arc::new(RwLock::new(HitRateMetrics::default())),
166        }
167    }
168
169    /// Get cached score for a user
170    pub fn get_score(&self, user_id: Uuid) -> Option<ReputationScore> {
171        let scores = self.scores.read().ok()?;
172        let cached = scores.get(&user_id);
173
174        if let Some(cached_entry) = cached {
175            if cached_entry.is_valid() {
176                // Record hit
177                if let Ok(mut metrics) = self.metrics.write() {
178                    metrics.score_hits += 1;
179                }
180                Some(cached_entry.score.clone())
181            } else {
182                // Expired entry - invalidate and record miss
183                drop(scores);
184                self.invalidate_score(user_id);
185                if let Ok(mut metrics) = self.metrics.write() {
186                    metrics.score_misses += 1;
187                }
188                None
189            }
190        } else {
191            // Not in cache - record miss
192            if let Ok(mut metrics) = self.metrics.write() {
193                metrics.score_misses += 1;
194            }
195            None
196        }
197    }
198
199    /// Cache a reputation score
200    pub fn set_score(&self, user_id: Uuid, score: ReputationScore) -> Result<(), ReputationError> {
201        let mut scores = self
202            .scores
203            .write()
204            .map_err(|_| ReputationError::Validation("Cache lock poisoned".to_string()))?;
205
206        scores.insert(
207            user_id,
208            CachedScore {
209                score,
210                cached_at: Utc::now(),
211                ttl_seconds: self.ttl_seconds,
212            },
213        );
214
215        Ok(())
216    }
217
218    /// Invalidate cached score for a user
219    pub fn invalidate_score(&self, user_id: Uuid) {
220        if let Ok(mut scores) = self.scores.write() {
221            scores.remove(&user_id);
222        }
223    }
224
225    /// Get cached tier for a user
226    pub fn get_tier(&self, user_id: Uuid) -> Option<(ReputationTier, Decimal)> {
227        let tiers = self.tiers.read().ok()?;
228        let cached = tiers.get(&user_id);
229
230        if let Some(cached_entry) = cached {
231            // Record hit
232            if let Ok(mut metrics) = self.metrics.write() {
233                metrics.tier_hits += 1;
234            }
235            Some((cached_entry.tier, cached_entry.score))
236        } else {
237            // Record miss
238            if let Ok(mut metrics) = self.metrics.write() {
239                metrics.tier_misses += 1;
240            }
241            None
242        }
243    }
244
245    /// Cache tier information
246    pub fn set_tier(
247        &self,
248        user_id: Uuid,
249        tier: ReputationTier,
250        score: Decimal,
251    ) -> Result<(), ReputationError> {
252        let mut tiers = self
253            .tiers
254            .write()
255            .map_err(|_| ReputationError::Validation("Cache lock poisoned".to_string()))?;
256
257        tiers.insert(
258            user_id,
259            CachedTier {
260                tier,
261                score,
262                cached_at: Utc::now(),
263            },
264        );
265
266        Ok(())
267    }
268
269    /// Invalidate cached tier for a user
270    pub fn invalidate_tier(&self, user_id: Uuid) {
271        if let Ok(mut tiers) = self.tiers.write() {
272            tiers.remove(&user_id);
273        }
274    }
275
276    /// Invalidate all cached data for a user
277    pub fn invalidate_user(&self, user_id: Uuid) {
278        self.invalidate_score(user_id);
279        self.invalidate_tier(user_id);
280    }
281
282    /// Clear all cached scores
283    pub fn clear_scores(&self) {
284        if let Ok(mut scores) = self.scores.write() {
285            scores.clear();
286        }
287    }
288
289    /// Clear all cached tiers
290    pub fn clear_tiers(&self) {
291        if let Ok(mut tiers) = self.tiers.write() {
292            tiers.clear();
293        }
294    }
295
296    /// Clear entire cache
297    pub fn clear_all(&self) {
298        self.clear_scores();
299        self.clear_tiers();
300    }
301
302    /// Remove expired entries from cache
303    pub fn cleanup_expired(&self) -> CacheCleanupStats {
304        let mut expired_scores = 0;
305
306        if let Ok(mut scores) = self.scores.write() {
307            scores.retain(|_, cached| {
308                let valid = cached.is_valid();
309                if !valid {
310                    expired_scores += 1;
311                }
312                valid
313            });
314        }
315
316        CacheCleanupStats {
317            expired_scores,
318            cleaned_at: Utc::now(),
319        }
320    }
321
322    /// Get cache statistics
323    pub fn get_stats(&self) -> CacheStats {
324        let scores_count = self.scores.read().map(|s| s.len()).unwrap_or(0);
325        let tiers_count = self.tiers.read().map(|t| t.len()).unwrap_or(0);
326
327        let metrics = self.metrics.read().ok();
328        let (score_hits, score_misses, tier_hits, tier_misses) = if let Some(m) = metrics {
329            (m.score_hits, m.score_misses, m.tier_hits, m.tier_misses)
330        } else {
331            (0, 0, 0, 0)
332        };
333
334        // Calculate hit rates
335        let score_hit_rate = if score_hits + score_misses > 0 {
336            score_hits as f64 / (score_hits + score_misses) as f64
337        } else {
338            0.0
339        };
340
341        let tier_hit_rate = if tier_hits + tier_misses > 0 {
342            tier_hits as f64 / (tier_hits + tier_misses) as f64
343        } else {
344            0.0
345        };
346
347        let total_hits = score_hits + tier_hits;
348        let total_ops = score_hits + score_misses + tier_hits + tier_misses;
349        let overall_hit_rate = if total_ops > 0 {
350            total_hits as f64 / total_ops as f64
351        } else {
352            0.0
353        };
354
355        CacheStats {
356            cached_scores: scores_count,
357            cached_tiers: tiers_count,
358            ttl_seconds: self.ttl_seconds,
359            score_hits,
360            score_misses,
361            tier_hits,
362            tier_misses,
363            score_hit_rate,
364            tier_hit_rate,
365            overall_hit_rate,
366        }
367    }
368
369    /// Reset cache metrics (hit/miss counters)
370    pub fn reset_metrics(&self) {
371        if let Ok(mut metrics) = self.metrics.write() {
372            metrics.score_hits = 0;
373            metrics.score_misses = 0;
374            metrics.tier_hits = 0;
375            metrics.tier_misses = 0;
376        }
377    }
378}
379
380/// Cache statistics with hit rate tracking
381#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
382pub struct CacheStats {
383    pub cached_scores: usize,
384    pub cached_tiers: usize,
385    pub ttl_seconds: i64,
386    pub score_hits: usize,
387    pub score_misses: usize,
388    pub tier_hits: usize,
389    pub tier_misses: usize,
390    pub score_hit_rate: f64,
391    pub tier_hit_rate: f64,
392    pub overall_hit_rate: f64,
393}
394
395impl CacheStats {
396    /// Check if cache hit rate is healthy (> 70%)
397    pub fn is_healthy(&self) -> bool {
398        self.overall_hit_rate > 0.7
399    }
400
401    /// Check if cache hit rate is poor (< 30%)
402    pub fn is_poor(&self) -> bool {
403        self.overall_hit_rate < 0.3
404    }
405
406    /// Get total number of cache operations
407    pub fn total_operations(&self) -> usize {
408        self.score_hits + self.score_misses + self.tier_hits + self.tier_misses
409    }
410}
411
412/// Cache cleanup statistics
413#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
414pub struct CacheCleanupStats {
415    pub expired_scores: usize,
416    pub cleaned_at: DateTime<Utc>,
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use rust_decimal_macros::dec;
423
424    #[test]
425    fn test_cached_score_is_valid() {
426        let score = ReputationScore {
427            user_id: Uuid::new_v4(),
428            overall_score: dec!(500),
429            tier: ReputationTier::Silver,
430            components: Default::default(),
431        };
432
433        let cached = CachedScore {
434            score,
435            cached_at: Utc::now(),
436            ttl_seconds: 300,
437        };
438
439        assert!(cached.is_valid());
440    }
441
442    #[test]
443    fn test_cached_score_expired() {
444        let score = ReputationScore {
445            user_id: Uuid::new_v4(),
446            overall_score: dec!(500),
447            tier: ReputationTier::Silver,
448            components: Default::default(),
449        };
450
451        let cached = CachedScore {
452            score,
453            cached_at: Utc::now() - chrono::Duration::seconds(400),
454            ttl_seconds: 300,
455        };
456
457        assert!(!cached.is_valid());
458    }
459
460    #[test]
461    fn test_cache_set_and_get_score() {
462        let cache = ReputationCache::new(300);
463        let user_id = Uuid::new_v4();
464        let score = ReputationScore {
465            user_id,
466            overall_score: dec!(500),
467            tier: ReputationTier::Silver,
468            components: Default::default(),
469        };
470
471        cache.set_score(user_id, score.clone()).unwrap();
472        let cached_score = cache.get_score(user_id).unwrap();
473
474        assert_eq!(cached_score.overall_score, score.overall_score);
475        assert_eq!(cached_score.tier, score.tier);
476    }
477
478    #[test]
479    fn test_cache_invalidate_score() {
480        let cache = ReputationCache::new(300);
481        let user_id = Uuid::new_v4();
482        let score = ReputationScore {
483            user_id,
484            overall_score: dec!(500),
485            tier: ReputationTier::Silver,
486            components: Default::default(),
487        };
488
489        cache.set_score(user_id, score).unwrap();
490        assert!(cache.get_score(user_id).is_some());
491
492        cache.invalidate_score(user_id);
493        assert!(cache.get_score(user_id).is_none());
494    }
495
496    #[test]
497    fn test_cache_tier() {
498        let cache = ReputationCache::new(300);
499        let user_id = Uuid::new_v4();
500        let tier = ReputationTier::Gold;
501        let score = dec!(750);
502
503        cache.set_tier(user_id, tier, score).unwrap();
504        let cached = cache.get_tier(user_id).unwrap();
505
506        assert_eq!(cached.0, tier);
507        assert_eq!(cached.1, score);
508    }
509
510    #[test]
511    fn test_cache_stats() {
512        let cache = ReputationCache::new(300);
513        let user_id = Uuid::new_v4();
514        let score = ReputationScore {
515            user_id,
516            overall_score: dec!(500),
517            tier: ReputationTier::Silver,
518            components: Default::default(),
519        };
520
521        cache.set_score(user_id, score).unwrap();
522        cache
523            .set_tier(user_id, ReputationTier::Silver, dec!(500))
524            .unwrap();
525
526        let stats = cache.get_stats();
527        assert_eq!(stats.cached_scores, 1);
528        assert_eq!(stats.cached_tiers, 1);
529        assert_eq!(stats.ttl_seconds, 300);
530    }
531
532    #[test]
533    fn test_cache_hit_rate_tracking() {
534        let cache = ReputationCache::new(300);
535        let user_id = Uuid::new_v4();
536        let score = ReputationScore {
537            user_id,
538            overall_score: dec!(500),
539            tier: ReputationTier::Silver,
540            components: Default::default(),
541        };
542
543        // Set cache
544        cache.set_score(user_id, score).unwrap();
545        cache
546            .set_tier(user_id, ReputationTier::Silver, dec!(500))
547            .unwrap();
548
549        // Hit the cache
550        assert!(cache.get_score(user_id).is_some());
551        assert!(cache.get_tier(user_id).is_some());
552
553        // Miss the cache
554        let other_user = Uuid::new_v4();
555        assert!(cache.get_score(other_user).is_none());
556        assert!(cache.get_tier(other_user).is_none());
557
558        let stats = cache.get_stats();
559        assert_eq!(stats.score_hits, 1);
560        assert_eq!(stats.score_misses, 1);
561        assert_eq!(stats.tier_hits, 1);
562        assert_eq!(stats.tier_misses, 1);
563        assert_eq!(stats.score_hit_rate, 0.5);
564        assert_eq!(stats.tier_hit_rate, 0.5);
565        assert_eq!(stats.overall_hit_rate, 0.5);
566    }
567
568    #[test]
569    fn test_cache_hit_rate_all_hits() {
570        let cache = ReputationCache::new(300);
571        let user_id = Uuid::new_v4();
572        let score = ReputationScore {
573            user_id,
574            overall_score: dec!(500),
575            tier: ReputationTier::Silver,
576            components: Default::default(),
577        };
578
579        cache.set_score(user_id, score).unwrap();
580        cache
581            .set_tier(user_id, ReputationTier::Silver, dec!(500))
582            .unwrap();
583
584        // Multiple hits
585        for _ in 0..5 {
586            assert!(cache.get_score(user_id).is_some());
587            assert!(cache.get_tier(user_id).is_some());
588        }
589
590        let stats = cache.get_stats();
591        assert_eq!(stats.score_hits, 5);
592        assert_eq!(stats.score_misses, 0);
593        assert_eq!(stats.tier_hits, 5);
594        assert_eq!(stats.tier_misses, 0);
595        assert_eq!(stats.score_hit_rate, 1.0);
596        assert_eq!(stats.tier_hit_rate, 1.0);
597        assert_eq!(stats.overall_hit_rate, 1.0);
598        assert!(stats.is_healthy());
599    }
600
601    #[test]
602    fn test_cache_hit_rate_all_misses() {
603        let cache = ReputationCache::new(300);
604
605        // All misses
606        for _ in 0..5 {
607            let user_id = Uuid::new_v4();
608            assert!(cache.get_score(user_id).is_none());
609            assert!(cache.get_tier(user_id).is_none());
610        }
611
612        let stats = cache.get_stats();
613        assert_eq!(stats.score_hits, 0);
614        assert_eq!(stats.score_misses, 5);
615        assert_eq!(stats.tier_hits, 0);
616        assert_eq!(stats.tier_misses, 5);
617        assert_eq!(stats.score_hit_rate, 0.0);
618        assert_eq!(stats.tier_hit_rate, 0.0);
619        assert_eq!(stats.overall_hit_rate, 0.0);
620        assert!(stats.is_poor());
621    }
622
623    #[test]
624    fn test_cache_reset_metrics() {
625        let cache = ReputationCache::new(300);
626        let user_id = Uuid::new_v4();
627        let score = ReputationScore {
628            user_id,
629            overall_score: dec!(500),
630            tier: ReputationTier::Silver,
631            components: Default::default(),
632        };
633
634        cache.set_score(user_id, score).unwrap();
635        cache.get_score(user_id);
636        cache.get_score(Uuid::new_v4());
637
638        let stats_before = cache.get_stats();
639        assert_eq!(stats_before.score_hits, 1);
640        assert_eq!(stats_before.score_misses, 1);
641
642        cache.reset_metrics();
643
644        let stats_after = cache.get_stats();
645        assert_eq!(stats_after.score_hits, 0);
646        assert_eq!(stats_after.score_misses, 0);
647        assert_eq!(stats_after.tier_hits, 0);
648        assert_eq!(stats_after.tier_misses, 0);
649    }
650
651    #[test]
652    fn test_cache_stats_helpers() {
653        let cache = ReputationCache::new(300);
654        let user_id = Uuid::new_v4();
655        let score = ReputationScore {
656            user_id,
657            overall_score: dec!(500),
658            tier: ReputationTier::Silver,
659            components: Default::default(),
660        };
661
662        cache.set_score(user_id, score).unwrap();
663
664        // Generate 8 hits and 2 misses (80% hit rate)
665        for _ in 0..8 {
666            cache.get_score(user_id);
667        }
668        for _ in 0..2 {
669            cache.get_score(Uuid::new_v4());
670        }
671
672        let stats = cache.get_stats();
673        assert_eq!(stats.total_operations(), 10);
674        assert!(stats.is_healthy()); // > 70%
675        assert!(!stats.is_poor());
676    }
677
678    #[test]
679    fn test_cache_clear_all() {
680        let cache = ReputationCache::new(300);
681        let user_id = Uuid::new_v4();
682        let score = ReputationScore {
683            user_id,
684            overall_score: dec!(500),
685            tier: ReputationTier::Silver,
686            components: Default::default(),
687        };
688
689        cache.set_score(user_id, score).unwrap();
690        cache
691            .set_tier(user_id, ReputationTier::Silver, dec!(500))
692            .unwrap();
693
694        cache.clear_all();
695
696        let stats = cache.get_stats();
697        assert_eq!(stats.cached_scores, 0);
698        assert_eq!(stats.cached_tiers, 0);
699    }
700
701    #[test]
702    fn test_remaining_ttl() {
703        let score = ReputationScore {
704            user_id: Uuid::new_v4(),
705            overall_score: dec!(500),
706            tier: ReputationTier::Silver,
707            components: Default::default(),
708        };
709
710        let cached = CachedScore {
711            score,
712            cached_at: Utc::now(),
713            ttl_seconds: 300,
714        };
715
716        let remaining = cached.remaining_ttl();
717        assert!(remaining > 0 && remaining <= 300);
718    }
719
720    // ========================================================================
721    // Property-Based Tests
722    // ========================================================================
723
724    mod proptest_cache {
725        use super::*;
726        use proptest::prelude::*;
727
728        // Strategy for generating valid TTL values (1-3600 seconds)
729        fn ttl_strategy() -> impl Strategy<Value = i64> {
730            1i64..=3600i64
731        }
732
733        // Strategy for generating valid scores (0-1000)
734        fn score_strategy() -> impl Strategy<Value = i64> {
735            0i64..=1000i64
736        }
737
738        proptest! {
739            /// Property: Setting and getting score should be consistent
740            #[test]
741            fn prop_cache_score_consistency(
742                score_val in score_strategy(),
743                ttl in ttl_strategy(),
744            ) {
745                let cache = ReputationCache::new(ttl);
746                let user_id = Uuid::new_v4();
747                let score = ReputationScore {
748                    user_id,
749                    overall_score: Decimal::new(score_val, 0),
750                    tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
751                    components: Default::default(),
752                };
753
754                cache.set_score(user_id, score.clone()).unwrap();
755                let retrieved = cache.get_score(user_id);
756
757                prop_assert!(retrieved.is_some());
758                if let Some(cached_score) = retrieved {
759                    prop_assert_eq!(cached_score.overall_score, score.overall_score);
760                    prop_assert_eq!(cached_score.user_id, user_id);
761                }
762            }
763
764            /// Property: Setting and getting tier should be consistent
765            #[test]
766            fn prop_cache_tier_consistency(
767                score_val in score_strategy(),
768                ttl in ttl_strategy(),
769            ) {
770                let cache = ReputationCache::new(ttl);
771                let user_id = Uuid::new_v4();
772                let tier = ReputationTier::from_score(Decimal::new(score_val, 0));
773                let score_dec = Decimal::new(score_val, 0);
774
775                cache.set_tier(user_id, tier, score_dec).unwrap();
776                let retrieved = cache.get_tier(user_id);
777
778                prop_assert!(retrieved.is_some());
779                if let Some((cached_tier, cached_score)) = retrieved {
780                    prop_assert_eq!(cached_tier, tier);
781                    prop_assert_eq!(cached_score, score_dec);
782                }
783            }
784
785            /// Property: Invalidating a user should remove all cached data
786            #[test]
787            fn prop_cache_invalidation(
788                score_val in score_strategy(),
789                ttl in ttl_strategy(),
790            ) {
791                let cache = ReputationCache::new(ttl);
792                let user_id = Uuid::new_v4();
793                let score = ReputationScore {
794                    user_id,
795                    overall_score: Decimal::new(score_val, 0),
796                    tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
797                    components: Default::default(),
798                };
799
800                cache.set_score(user_id, score.clone()).unwrap();
801                cache.set_tier(user_id, score.tier, score.overall_score).unwrap();
802
803                cache.invalidate_user(user_id);
804
805                prop_assert!(cache.get_score(user_id).is_none());
806                prop_assert!(cache.get_tier(user_id).is_none());
807            }
808
809            /// Property: Cache stats should accurately reflect cached items
810            #[test]
811            fn prop_cache_stats_accuracy(
812                num_users in 1usize..=10usize,
813                score_val in score_strategy(),
814                ttl in ttl_strategy(),
815            ) {
816                let cache = ReputationCache::new(ttl);
817                let user_ids: Vec<Uuid> = (0..num_users).map(|_| Uuid::new_v4()).collect();
818
819                for user_id in &user_ids {
820                    let score = ReputationScore {
821                        user_id: *user_id,
822                        overall_score: Decimal::new(score_val, 0),
823                        tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
824                        components: Default::default(),
825                    };
826                    cache.set_score(*user_id, score).unwrap();
827                }
828
829                let stats = cache.get_stats();
830                prop_assert_eq!(stats.cached_scores, num_users);
831            }
832
833            /// Property: Clear all should remove all cached entries
834            #[test]
835            fn prop_cache_clear_all(
836                num_users in 1usize..=10usize,
837                score_val in score_strategy(),
838                ttl in ttl_strategy(),
839            ) {
840                let cache = ReputationCache::new(ttl);
841                let user_ids: Vec<Uuid> = (0..num_users).map(|_| Uuid::new_v4()).collect();
842
843                for user_id in &user_ids {
844                    let score = ReputationScore {
845                        user_id: *user_id,
846                        overall_score: Decimal::new(score_val, 0),
847                        tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
848                        components: Default::default(),
849                    };
850                    cache.set_score(*user_id, score).unwrap();
851                    cache.set_tier(*user_id, ReputationTier::from_score(Decimal::new(score_val, 0)), Decimal::new(score_val, 0)).unwrap();
852                }
853
854                cache.clear_all();
855
856                let stats = cache.get_stats();
857                prop_assert_eq!(stats.cached_scores, 0);
858                prop_assert_eq!(stats.cached_tiers, 0);
859            }
860
861            /// Property: Remaining TTL should always be <= original TTL
862            #[test]
863            fn prop_remaining_ttl_bounds(
864                score_val in score_strategy(),
865                ttl in ttl_strategy(),
866            ) {
867                let score = ReputationScore {
868                    user_id: Uuid::new_v4(),
869                    overall_score: Decimal::new(score_val, 0),
870                    tier: ReputationTier::from_score(Decimal::new(score_val, 0)),
871                    components: Default::default(),
872                };
873
874                let cached = CachedScore {
875                    score,
876                    cached_at: Utc::now(),
877                    ttl_seconds: ttl,
878                };
879
880                let remaining = cached.remaining_ttl();
881                prop_assert!(remaining >= 0);
882                prop_assert!(remaining <= ttl);
883            }
884        }
885    }
886}