Skip to main content

mockforge_core/
cache.rs

1//! High-performance caching utilities for MockForge
2//!
3//! This module provides various caching strategies to optimize
4//! performance for frequently accessed data.
5
6use std::collections::HashMap;
7use std::hash::Hash;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::RwLock;
11
12/// Cache entry with expiration support
13#[derive(Debug, Clone)]
14struct CacheEntry<V> {
15    value: V,
16    expires_at: Option<Instant>,
17    access_count: u64,
18    last_accessed: Instant,
19}
20
21impl<V> CacheEntry<V> {
22    fn new(value: V, ttl: Option<Duration>) -> Self {
23        let now = Instant::now();
24        Self {
25            value,
26            expires_at: ttl.map(|duration| now + duration),
27            access_count: 0,
28            last_accessed: now,
29        }
30    }
31
32    fn is_expired(&self) -> bool {
33        self.expires_at.is_some_and(|expires_at| Instant::now() > expires_at)
34    }
35
36    fn access(&mut self) -> &V {
37        self.access_count += 1;
38        self.last_accessed = Instant::now();
39        &self.value
40    }
41}
42
43/// High-performance in-memory cache with TTL and LRU eviction
44#[derive(Debug)]
45pub struct Cache<K, V> {
46    storage: Arc<RwLock<HashMap<K, CacheEntry<V>>>>,
47    max_size: usize,
48    default_ttl: Option<Duration>,
49    stats: Arc<RwLock<CacheStats>>,
50}
51
52/// Statistics for cache performance tracking
53#[derive(Debug, Default, Clone)]
54pub struct CacheStats {
55    /// Number of cache hits (successful lookups)
56    pub hits: u64,
57    /// Number of cache misses (failed lookups)
58    pub misses: u64,
59    /// Number of entries evicted due to size limits
60    pub evictions: u64,
61    /// Number of entries expired due to TTL
62    pub expirations: u64,
63    /// Total number of insertions
64    pub insertions: u64,
65}
66
67impl<K: Hash + Eq + Clone, V: Clone> Cache<K, V> {
68    /// Create a new cache with specified maximum size
69    pub fn new(max_size: usize) -> Self {
70        Self {
71            storage: Arc::new(RwLock::new(HashMap::new())),
72            max_size,
73            default_ttl: None,
74            stats: Arc::new(RwLock::new(CacheStats::default())),
75        }
76    }
77
78    /// Create a new cache with TTL support
79    pub fn with_ttl(max_size: usize, default_ttl: Duration) -> Self {
80        Self {
81            storage: Arc::new(RwLock::new(HashMap::new())),
82            max_size,
83            default_ttl: Some(default_ttl),
84            stats: Arc::new(RwLock::new(CacheStats::default())),
85        }
86    }
87
88    /// Insert a value with optional custom TTL
89    pub async fn insert(&self, key: K, value: V, ttl: Option<Duration>) {
90        let mut storage = self.storage.write().await;
91        let mut stats = self.stats.write().await;
92
93        // Use provided TTL or default TTL
94        let effective_ttl = ttl.or(self.default_ttl);
95
96        // Clean up expired entries
97        self.cleanup_expired(&mut storage, &mut stats).await;
98
99        // Evict LRU entries if at capacity
100        if storage.len() >= self.max_size && !storage.contains_key(&key) {
101            self.evict_lru(&mut storage, &mut stats).await;
102        }
103
104        storage.insert(key, CacheEntry::new(value, effective_ttl));
105        stats.insertions += 1;
106    }
107
108    /// Get a value from the cache
109    pub async fn get(&self, key: &K) -> Option<V> {
110        let mut storage = self.storage.write().await;
111        let mut stats = self.stats.write().await;
112
113        if let Some(entry) = storage.get_mut(key) {
114            if entry.is_expired() {
115                storage.remove(key);
116                stats.expirations += 1;
117                stats.misses += 1;
118                return None;
119            }
120
121            stats.hits += 1;
122            Some(entry.access().clone())
123        } else {
124            stats.misses += 1;
125            None
126        }
127    }
128
129    /// Check if a key exists in the cache (without updating access stats)
130    pub async fn contains_key(&self, key: &K) -> bool {
131        let storage = self.storage.read().await;
132        if let Some(entry) = storage.get(key) {
133            !entry.is_expired()
134        } else {
135            false
136        }
137    }
138
139    /// Remove a key from the cache
140    pub async fn remove(&self, key: &K) -> Option<V> {
141        let mut storage = self.storage.write().await;
142        storage.remove(key).map(|entry| entry.value)
143    }
144
145    /// Clear all entries from the cache
146    pub async fn clear(&self) {
147        let mut storage = self.storage.write().await;
148        storage.clear();
149    }
150
151    /// Get current cache size
152    pub async fn len(&self) -> usize {
153        let storage = self.storage.read().await;
154        storage.len()
155    }
156
157    /// Check if cache is empty
158    pub async fn is_empty(&self) -> bool {
159        let storage = self.storage.read().await;
160        storage.is_empty()
161    }
162
163    /// Get cache statistics
164    pub async fn stats(&self) -> CacheStats {
165        let stats = self.stats.read().await;
166        stats.clone()
167    }
168
169    /// Reset cache statistics
170    pub async fn reset_stats(&self) {
171        let mut stats = self.stats.write().await;
172        *stats = CacheStats::default();
173    }
174
175    /// Get or insert a value using a closure
176    pub async fn get_or_insert<F, Fut>(&self, key: K, f: F) -> V
177    where
178        F: FnOnce() -> Fut,
179        Fut: std::future::Future<Output = V>,
180    {
181        if let Some(value) = self.get(&key).await {
182            return value;
183        }
184
185        let value = f().await;
186        self.insert(key, value.clone(), None).await;
187        value
188    }
189
190    /// Get or insert a value with custom TTL using a closure
191    pub async fn get_or_insert_with_ttl<F, Fut>(&self, key: K, f: F, ttl: Duration) -> V
192    where
193        F: FnOnce() -> Fut,
194        Fut: std::future::Future<Output = V>,
195    {
196        if let Some(value) = self.get(&key).await {
197            return value;
198        }
199
200        let value = f().await;
201        self.insert(key, value.clone(), Some(ttl)).await;
202        value
203    }
204
205    /// Cleanup expired entries (internal)
206    async fn cleanup_expired(
207        &self,
208        storage: &mut HashMap<K, CacheEntry<V>>,
209        stats: &mut CacheStats,
210    ) {
211        let expired_keys: Vec<K> = storage
212            .iter()
213            .filter_map(|(k, v)| {
214                if v.is_expired() {
215                    Some(k.clone())
216                } else {
217                    None
218                }
219            })
220            .collect();
221
222        for key in expired_keys {
223            storage.remove(&key);
224            stats.expirations += 1;
225        }
226    }
227
228    /// Evict least recently used entry (internal)
229    async fn evict_lru(&self, storage: &mut HashMap<K, CacheEntry<V>>, stats: &mut CacheStats) {
230        if let Some((lru_key, _)) = storage
231            .iter()
232            .min_by_key(|(_, entry)| entry.last_accessed)
233            .map(|(k, v)| (k.clone(), v.clone()))
234        {
235            storage.remove(&lru_key);
236            stats.evictions += 1;
237        }
238    }
239}
240
241/// Response cache specifically optimized for HTTP responses
242#[derive(Debug)]
243pub struct ResponseCache {
244    cache: Cache<String, CachedResponse>,
245}
246
247/// Cached HTTP response data
248#[derive(Debug, Clone)]
249pub struct CachedResponse {
250    /// HTTP status code
251    pub status_code: u16,
252    /// Response headers
253    pub headers: HashMap<String, String>,
254    /// Response body content
255    pub body: String,
256    /// Content-Type header value, if present
257    pub content_type: Option<String>,
258}
259
260impl ResponseCache {
261    /// Create a new response cache
262    pub fn new(max_size: usize, ttl: Duration) -> Self {
263        Self {
264            cache: Cache::with_ttl(max_size, ttl),
265        }
266    }
267
268    /// Generate cache key from request parameters
269    pub fn generate_key(
270        method: &str,
271        path: &str,
272        query: &str,
273        headers: &HashMap<String, String>,
274    ) -> String {
275        use std::collections::hash_map::DefaultHasher;
276        use std::hash::Hasher;
277
278        let mut hasher = DefaultHasher::new();
279        hasher.write(method.as_bytes());
280        hasher.write(path.as_bytes());
281        hasher.write(query.as_bytes());
282
283        // Include relevant headers in cache key
284        let mut sorted_headers: Vec<_> = headers.iter().collect();
285        sorted_headers.sort_by_key(|(k, _)| *k);
286        for (key, value) in sorted_headers {
287            if key.to_lowercase() != "authorization" && !key.to_lowercase().starts_with("x-") {
288                hasher.write(key.as_bytes());
289                hasher.write(value.as_bytes());
290            }
291        }
292
293        format!("resp_{}_{}", hasher.finish(), path.len())
294    }
295
296    /// Cache a response
297    pub async fn cache_response(&self, key: String, response: CachedResponse) {
298        self.cache.insert(key, response, None).await;
299    }
300
301    /// Get cached response
302    pub async fn get_response(&self, key: &str) -> Option<CachedResponse> {
303        self.cache.get(&key.to_string()).await
304    }
305
306    /// Clear all entries from the response cache
307    pub async fn clear(&self) {
308        self.cache.clear().await;
309    }
310
311    /// Get cache statistics
312    pub async fn stats(&self) -> CacheStats {
313        self.cache.stats().await
314    }
315}
316
317/// Template cache for compiled templates
318#[derive(Debug)]
319pub struct TemplateCache {
320    cache: Cache<String, CompiledTemplate>,
321}
322
323/// Compiled template with metadata for caching
324#[derive(Debug, Clone)]
325pub struct CompiledTemplate {
326    /// The compiled template string
327    pub template: String,
328    /// List of variable names used in the template
329    pub variables: Vec<String>,
330    /// Timestamp when the template was compiled
331    pub compiled_at: Instant,
332}
333
334impl TemplateCache {
335    /// Create a new template cache
336    pub fn new(max_size: usize) -> Self {
337        Self {
338            cache: Cache::new(max_size),
339        }
340    }
341
342    /// Cache a compiled template
343    pub async fn cache_template(&self, key: String, template: String, variables: Vec<String>) {
344        let compiled = CompiledTemplate {
345            template,
346            variables,
347            compiled_at: Instant::now(),
348        };
349        self.cache.insert(key, compiled, None).await;
350    }
351
352    /// Get cached template
353    pub async fn get_template(&self, key: &str) -> Option<CompiledTemplate> {
354        self.cache.get(&key.to_string()).await
355    }
356
357    /// Clear all entries from the template cache
358    pub async fn clear(&self) {
359        self.cache.clear().await;
360    }
361
362    /// Get cache statistics
363    pub async fn stats(&self) -> CacheStats {
364        self.cache.stats().await
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use tokio::time::sleep;
372
373    // ==================== Basic Cache Operations ====================
374
375    #[tokio::test]
376    async fn test_basic_cache_operations() {
377        let cache = Cache::new(3);
378
379        cache.insert("key1".to_string(), "value1".to_string(), None).await;
380        cache.insert("key2".to_string(), "value2".to_string(), None).await;
381
382        assert_eq!(cache.get(&"key1".to_string()).await, Some("value1".to_string()));
383        assert_eq!(cache.get(&"key2".to_string()).await, Some("value2".to_string()));
384        assert_eq!(cache.get(&"key3".to_string()).await, None);
385
386        assert_eq!(cache.len().await, 2);
387        assert!(!cache.is_empty().await);
388    }
389
390    #[tokio::test]
391    async fn test_cache_new() {
392        let cache: Cache<String, String> = Cache::new(100);
393        assert!(cache.is_empty().await);
394        assert_eq!(cache.len().await, 0);
395    }
396
397    #[tokio::test]
398    async fn test_cache_with_ttl() {
399        let cache: Cache<String, String> = Cache::with_ttl(100, Duration::from_secs(60));
400        assert!(cache.is_empty().await);
401    }
402
403    #[tokio::test]
404    async fn test_cache_contains_key() {
405        let cache = Cache::new(10);
406        cache.insert("key1".to_string(), "value1".to_string(), None).await;
407
408        assert!(cache.contains_key(&"key1".to_string()).await);
409        assert!(!cache.contains_key(&"key2".to_string()).await);
410    }
411
412    #[tokio::test]
413    async fn test_cache_remove() {
414        let cache = Cache::new(10);
415        cache.insert("key1".to_string(), "value1".to_string(), None).await;
416
417        let removed = cache.remove(&"key1".to_string()).await;
418        assert_eq!(removed, Some("value1".to_string()));
419        assert!(!cache.contains_key(&"key1".to_string()).await);
420
421        // Remove non-existent key
422        let removed2 = cache.remove(&"key2".to_string()).await;
423        assert_eq!(removed2, None);
424    }
425
426    #[tokio::test]
427    async fn test_cache_clear() {
428        let cache = Cache::new(10);
429        cache.insert("key1".to_string(), "value1".to_string(), None).await;
430        cache.insert("key2".to_string(), "value2".to_string(), None).await;
431
432        assert_eq!(cache.len().await, 2);
433        cache.clear().await;
434        assert_eq!(cache.len().await, 0);
435        assert!(cache.is_empty().await);
436    }
437
438    #[tokio::test]
439    async fn test_cache_overwrite() {
440        let cache = Cache::new(10);
441        cache.insert("key1".to_string(), "value1".to_string(), None).await;
442        cache.insert("key1".to_string(), "value2".to_string(), None).await;
443
444        assert_eq!(cache.get(&"key1".to_string()).await, Some("value2".to_string()));
445        assert_eq!(cache.len().await, 1);
446    }
447
448    // ==================== TTL Tests ====================
449
450    #[tokio::test]
451    async fn test_ttl_expiration() {
452        let cache = Cache::with_ttl(10, Duration::from_millis(200));
453
454        cache.insert("key1".to_string(), "value1".to_string(), None).await;
455        assert_eq!(cache.get(&"key1".to_string()).await, Some("value1".to_string()));
456
457        sleep(Duration::from_millis(300)).await;
458        assert_eq!(cache.get(&"key1".to_string()).await, None);
459    }
460
461    #[tokio::test]
462    async fn test_custom_ttl_per_entry() {
463        let cache = Cache::new(10);
464
465        // Insert with custom TTL
466        cache
467            .insert(
468                "short".to_string(),
469                "short_lived".to_string(),
470                Some(Duration::from_millis(200)),
471            )
472            .await;
473        cache
474            .insert("long".to_string(), "long_lived".to_string(), Some(Duration::from_secs(60)))
475            .await;
476
477        assert_eq!(cache.get(&"short".to_string()).await, Some("short_lived".to_string()));
478        assert_eq!(cache.get(&"long".to_string()).await, Some("long_lived".to_string()));
479
480        // Wait for short TTL to expire
481        sleep(Duration::from_millis(300)).await;
482
483        assert_eq!(cache.get(&"short".to_string()).await, None);
484        assert_eq!(cache.get(&"long".to_string()).await, Some("long_lived".to_string()));
485    }
486
487    #[tokio::test]
488    async fn test_contains_key_respects_ttl() {
489        let cache = Cache::with_ttl(10, Duration::from_millis(200));
490        cache.insert("key".to_string(), "value".to_string(), None).await;
491
492        assert!(cache.contains_key(&"key".to_string()).await);
493
494        sleep(Duration::from_millis(300)).await;
495
496        assert!(!cache.contains_key(&"key".to_string()).await);
497    }
498
499    // ==================== LRU Eviction Tests ====================
500
501    #[tokio::test]
502    async fn test_lru_eviction() {
503        let cache = Cache::new(2);
504
505        cache.insert("key1".to_string(), "value1".to_string(), None).await;
506        cache.insert("key2".to_string(), "value2".to_string(), None).await;
507
508        // Access key1 to make it more recently used
509        cache.get(&"key1".to_string()).await;
510
511        // Insert key3, should evict key2 (least recently used)
512        cache.insert("key3".to_string(), "value3".to_string(), None).await;
513
514        assert_eq!(cache.get(&"key1".to_string()).await, Some("value1".to_string()));
515        assert_eq!(cache.get(&"key2".to_string()).await, None);
516        assert_eq!(cache.get(&"key3".to_string()).await, Some("value3".to_string()));
517    }
518
519    #[tokio::test]
520    async fn test_eviction_stats() {
521        let cache = Cache::new(2);
522
523        cache.insert("key1".to_string(), "value1".to_string(), None).await;
524        cache.insert("key2".to_string(), "value2".to_string(), None).await;
525        cache.insert("key3".to_string(), "value3".to_string(), None).await;
526
527        let stats = cache.stats().await;
528        assert_eq!(stats.evictions, 1);
529    }
530
531    #[tokio::test]
532    async fn test_no_eviction_when_replacing() {
533        let cache = Cache::new(2);
534
535        cache.insert("key1".to_string(), "value1".to_string(), None).await;
536        cache.insert("key2".to_string(), "value2".to_string(), None).await;
537        // Replace existing key, shouldn't evict
538        cache.insert("key1".to_string(), "updated".to_string(), None).await;
539
540        let stats = cache.stats().await;
541        assert_eq!(stats.evictions, 0);
542        assert_eq!(cache.len().await, 2);
543    }
544
545    // ==================== Stats Tests ====================
546
547    #[tokio::test]
548    async fn test_cache_stats() {
549        let cache = Cache::new(10);
550
551        cache.insert("key1".to_string(), "value1".to_string(), None).await;
552        cache.get(&"key1".to_string()).await; // Hit
553        cache.get(&"key2".to_string()).await; // Miss
554
555        let stats = cache.stats().await;
556        assert_eq!(stats.hits, 1);
557        assert_eq!(stats.misses, 1);
558        assert_eq!(stats.insertions, 1);
559    }
560
561    #[tokio::test]
562    async fn test_reset_stats() {
563        let cache = Cache::new(10);
564
565        cache.insert("key1".to_string(), "value1".to_string(), None).await;
566        cache.get(&"key1".to_string()).await;
567        cache.get(&"key2".to_string()).await;
568
569        let stats = cache.stats().await;
570        assert_eq!(stats.hits, 1);
571        assert_eq!(stats.misses, 1);
572
573        cache.reset_stats().await;
574
575        let stats_after = cache.stats().await;
576        assert_eq!(stats_after.hits, 0);
577        assert_eq!(stats_after.misses, 0);
578        assert_eq!(stats_after.insertions, 0);
579    }
580
581    #[tokio::test]
582    async fn test_expiration_stats() {
583        let cache = Cache::with_ttl(10, Duration::from_millis(20));
584
585        cache.insert("key".to_string(), "value".to_string(), None).await;
586        sleep(Duration::from_millis(30)).await;
587        cache.get(&"key".to_string()).await; // Should trigger expiration
588
589        let stats = cache.stats().await;
590        assert_eq!(stats.expirations, 1);
591    }
592
593    // ==================== get_or_insert Tests ====================
594
595    #[tokio::test]
596    async fn test_get_or_insert_miss() {
597        let cache = Cache::new(10);
598
599        let value = cache
600            .get_or_insert("key".to_string(), || async { "computed_value".to_string() })
601            .await;
602
603        assert_eq!(value, "computed_value".to_string());
604        assert_eq!(cache.get(&"key".to_string()).await, Some("computed_value".to_string()));
605    }
606
607    #[tokio::test]
608    async fn test_get_or_insert_hit() {
609        let cache = Cache::new(10);
610        cache.insert("key".to_string(), "existing_value".to_string(), None).await;
611
612        let value = cache
613            .get_or_insert("key".to_string(), || async { "should_not_be_used".to_string() })
614            .await;
615
616        assert_eq!(value, "existing_value".to_string());
617    }
618
619    #[tokio::test]
620    async fn test_get_or_insert_with_ttl() {
621        let cache = Cache::new(10);
622
623        let value = cache
624            .get_or_insert_with_ttl(
625                "key".to_string(),
626                || async { "computed".to_string() },
627                Duration::from_millis(30),
628            )
629            .await;
630
631        assert_eq!(value, "computed".to_string());
632
633        // Value should exist before TTL expires
634        assert!(cache.contains_key(&"key".to_string()).await);
635
636        // Wait for TTL
637        sleep(Duration::from_millis(50)).await;
638
639        assert!(!cache.contains_key(&"key".to_string()).await);
640    }
641
642    // ==================== ResponseCache Tests ====================
643
644    #[tokio::test]
645    async fn test_response_cache() {
646        let response_cache = ResponseCache::new(100, Duration::from_secs(300));
647
648        let headers = HashMap::new();
649        let key = ResponseCache::generate_key("GET", "/api/users", "", &headers);
650
651        let response = CachedResponse {
652            status_code: 200,
653            headers: HashMap::new(),
654            body: "test response".to_string(),
655            content_type: Some("application/json".to_string()),
656        };
657
658        response_cache.cache_response(key.clone(), response.clone()).await;
659        let cached = response_cache.get_response(&key).await;
660
661        assert!(cached.is_some());
662        assert_eq!(cached.unwrap().body, "test response");
663    }
664
665    #[tokio::test]
666    async fn test_response_cache_key_generation() {
667        let headers1 = HashMap::new();
668        let headers2 = HashMap::new();
669
670        // Same request params should generate same key
671        let key1 = ResponseCache::generate_key("GET", "/api/users", "page=1", &headers1);
672        let key2 = ResponseCache::generate_key("GET", "/api/users", "page=1", &headers2);
673        assert_eq!(key1, key2);
674
675        // Different method should generate different key
676        let key3 = ResponseCache::generate_key("POST", "/api/users", "page=1", &headers1);
677        assert_ne!(key1, key3);
678
679        // Different path should generate different key
680        let key4 = ResponseCache::generate_key("GET", "/api/items", "page=1", &headers1);
681        assert_ne!(key1, key4);
682
683        // Different query should generate different key
684        let key5 = ResponseCache::generate_key("GET", "/api/users", "page=2", &headers1);
685        assert_ne!(key1, key5);
686    }
687
688    #[tokio::test]
689    async fn test_response_cache_key_excludes_auth_headers() {
690        let mut headers_without_auth = HashMap::new();
691        headers_without_auth.insert("accept".to_string(), "application/json".to_string());
692
693        let mut headers_with_auth = headers_without_auth.clone();
694        headers_with_auth.insert("authorization".to_string(), "Bearer token123".to_string());
695
696        // Authorization header should be excluded from key
697        let key1 = ResponseCache::generate_key("GET", "/api/users", "", &headers_without_auth);
698        let key2 = ResponseCache::generate_key("GET", "/api/users", "", &headers_with_auth);
699
700        assert_eq!(key1, key2);
701    }
702
703    #[tokio::test]
704    async fn test_response_cache_key_excludes_x_headers() {
705        let mut headers1 = HashMap::new();
706        headers1.insert("accept".to_string(), "application/json".to_string());
707
708        let mut headers2 = headers1.clone();
709        headers2.insert("x-request-id".to_string(), "unique-id-123".to_string());
710        headers2.insert("x-correlation-id".to_string(), "corr-456".to_string());
711
712        let key1 = ResponseCache::generate_key("GET", "/api/users", "", &headers1);
713        let key2 = ResponseCache::generate_key("GET", "/api/users", "", &headers2);
714
715        assert_eq!(key1, key2);
716    }
717
718    #[tokio::test]
719    async fn test_response_cache_stats() {
720        let response_cache = ResponseCache::new(10, Duration::from_secs(60));
721
722        let response = CachedResponse {
723            status_code: 200,
724            headers: HashMap::new(),
725            body: "test".to_string(),
726            content_type: None,
727        };
728
729        response_cache.cache_response("key1".to_string(), response).await;
730        response_cache.get_response("key1").await; // Hit
731        response_cache.get_response("key2").await; // Miss
732
733        let stats = response_cache.stats().await;
734        assert_eq!(stats.hits, 1);
735        assert_eq!(stats.misses, 1);
736    }
737
738    // ==================== TemplateCache Tests ====================
739
740    #[tokio::test]
741    async fn test_template_cache_new() {
742        let template_cache = TemplateCache::new(100);
743        assert_eq!(template_cache.stats().await.insertions, 0);
744    }
745
746    #[tokio::test]
747    async fn test_template_cache_operations() {
748        let template_cache = TemplateCache::new(100);
749
750        template_cache
751            .cache_template(
752                "greeting".to_string(),
753                "Hello, {{name}}!".to_string(),
754                vec!["name".to_string()],
755            )
756            .await;
757
758        let cached = template_cache.get_template("greeting").await;
759        assert!(cached.is_some());
760
761        let template = cached.unwrap();
762        assert_eq!(template.template, "Hello, {{name}}!");
763        assert_eq!(template.variables, vec!["name".to_string()]);
764    }
765
766    #[tokio::test]
767    async fn test_template_cache_miss() {
768        let template_cache = TemplateCache::new(100);
769
770        let cached = template_cache.get_template("nonexistent").await;
771        assert!(cached.is_none());
772    }
773
774    #[tokio::test]
775    async fn test_template_cache_stats() {
776        let template_cache = TemplateCache::new(10);
777
778        template_cache
779            .cache_template("key".to_string(), "template".to_string(), vec![])
780            .await;
781
782        template_cache.get_template("key").await; // Hit
783        template_cache.get_template("missing").await; // Miss
784
785        let stats = template_cache.stats().await;
786        assert_eq!(stats.hits, 1);
787        assert_eq!(stats.misses, 1);
788        assert_eq!(stats.insertions, 1);
789    }
790
791    // ==================== CacheStats Tests ====================
792
793    #[test]
794    fn test_cache_stats_default() {
795        let stats = CacheStats::default();
796        assert_eq!(stats.hits, 0);
797        assert_eq!(stats.misses, 0);
798        assert_eq!(stats.evictions, 0);
799        assert_eq!(stats.expirations, 0);
800        assert_eq!(stats.insertions, 0);
801    }
802
803    #[test]
804    fn test_cache_stats_clone() {
805        let stats = CacheStats {
806            hits: 10,
807            misses: 5,
808            ..Default::default()
809        };
810
811        let cloned = stats.clone();
812        assert_eq!(cloned.hits, 10);
813        assert_eq!(cloned.misses, 5);
814    }
815
816    #[test]
817    fn test_cache_stats_debug() {
818        let stats = CacheStats::default();
819        let debug_str = format!("{:?}", stats);
820        assert!(debug_str.contains("CacheStats"));
821        assert!(debug_str.contains("hits"));
822    }
823
824    // ==================== CachedResponse Tests ====================
825
826    #[test]
827    fn test_cached_response_clone() {
828        let response = CachedResponse {
829            status_code: 200,
830            headers: HashMap::new(),
831            body: "test".to_string(),
832            content_type: Some("application/json".to_string()),
833        };
834
835        let cloned = response.clone();
836        assert_eq!(cloned.status_code, 200);
837        assert_eq!(cloned.body, "test");
838        assert_eq!(cloned.content_type, Some("application/json".to_string()));
839    }
840
841    #[test]
842    fn test_cached_response_debug() {
843        let response = CachedResponse {
844            status_code: 404,
845            headers: HashMap::new(),
846            body: "not found".to_string(),
847            content_type: None,
848        };
849
850        let debug_str = format!("{:?}", response);
851        assert!(debug_str.contains("CachedResponse"));
852        assert!(debug_str.contains("404"));
853    }
854
855    // ==================== CompiledTemplate Tests ====================
856
857    #[test]
858    fn test_compiled_template_clone() {
859        let template = CompiledTemplate {
860            template: "Hello, {{name}}!".to_string(),
861            variables: vec!["name".to_string()],
862            compiled_at: Instant::now(),
863        };
864
865        let cloned = template.clone();
866        assert_eq!(cloned.template, "Hello, {{name}}!");
867        assert_eq!(cloned.variables, vec!["name".to_string()]);
868    }
869
870    #[test]
871    fn test_compiled_template_debug() {
872        let template = CompiledTemplate {
873            template: "test".to_string(),
874            variables: vec![],
875            compiled_at: Instant::now(),
876        };
877
878        let debug_str = format!("{:?}", template);
879        assert!(debug_str.contains("CompiledTemplate"));
880        assert!(debug_str.contains("test"));
881    }
882
883    // ==================== Edge Cases ====================
884
885    #[tokio::test]
886    async fn test_cache_with_zero_size() {
887        // Zero-size cache should handle gracefully
888        let cache = Cache::new(0);
889        cache.insert("key".to_string(), "value".to_string(), None).await;
890        // May or may not be stored depending on implementation
891    }
892
893    #[tokio::test]
894    async fn test_cache_with_numeric_keys() {
895        let cache = Cache::new(10);
896        cache.insert(1, "one".to_string(), None).await;
897        cache.insert(2, "two".to_string(), None).await;
898
899        assert_eq!(cache.get(&1).await, Some("one".to_string()));
900        assert_eq!(cache.get(&2).await, Some("two".to_string()));
901    }
902
903    #[tokio::test]
904    async fn test_cache_with_complex_values() {
905        let cache: Cache<String, Vec<u8>> = Cache::new(10);
906        cache.insert("bytes".to_string(), vec![1, 2, 3, 4, 5], None).await;
907
908        let retrieved = cache.get(&"bytes".to_string()).await;
909        assert_eq!(retrieved, Some(vec![1, 2, 3, 4, 5]));
910    }
911
912    #[tokio::test]
913    async fn test_multiple_expirations_cleanup() {
914        let cache = Cache::with_ttl(10, Duration::from_millis(20));
915
916        cache.insert("key1".to_string(), "v1".to_string(), None).await;
917        cache.insert("key2".to_string(), "v2".to_string(), None).await;
918        cache.insert("key3".to_string(), "v3".to_string(), None).await;
919
920        sleep(Duration::from_millis(30)).await;
921
922        // All should be expired, but insert triggers cleanup
923        cache.insert("new".to_string(), "new_val".to_string(), None).await;
924
925        let stats = cache.stats().await;
926        assert!(stats.expirations >= 3);
927    }
928}