priority-lfu 0.3.0

A high-performance, concurrent, in-memory cache with W-TinyLFU eviction policy and weight-based prioritization.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
use std::sync::Arc;
use std::thread;

use priority_lfu::{Cache, CacheBuilder, CacheKey, CachePolicy, DeepSizeOf};

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct StringKey(String);

impl CacheKey for StringKey {
	type Value = StringValue;

	fn policy(&self) -> CachePolicy {
		CachePolicy::Standard
	}
}

#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct StringValue {
	data: String,
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct IntKey(u64);

impl CacheKey for IntKey {
	type Value = IntValue;

	fn policy(&self) -> CachePolicy {
		CachePolicy::Standard
	}
}

#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct IntValue(i64);

// Policy-specific helper key types for testing eviction behavior
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct VolatileKey(u64);

impl CacheKey for VolatileKey {
	type Value = IntValue;

	fn policy(&self) -> CachePolicy {
		CachePolicy::Volatile
	}
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct StandardKey(u64);

impl CacheKey for StandardKey {
	type Value = IntValue;

	fn policy(&self) -> CachePolicy {
		CachePolicy::Standard
	}
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct CriticalKey(u64);

impl CacheKey for CriticalKey {
	type Value = IntValue;

	fn policy(&self) -> CachePolicy {
		CachePolicy::Critical
	}
}

#[test]
fn test_basic_operations() {
	let cache = Cache::new(10240);

	let key = StringKey("test".to_string());
	let value = StringValue {
		data: "hello world".to_string(),
	};

	// Insert
	cache.insert(key.clone(), value.clone());
	assert!(cache.contains(&key));

	// Get with guard
	{
		let guard = cache.get(&key).expect("key should exist");
		assert_eq!(*guard, value);
	}

	// Get clone
	let cloned = cache.get_clone(&key).expect("key should exist");
	assert_eq!(cloned, value);

	// Remove
	let removed = cache.remove(&key).expect("key should exist");
	assert_eq!(removed, value);
	assert!(!cache.contains(&key));
}

#[test]
fn test_heterogeneous_types() {
	let cache = Cache::new(10240);

	let str_key = StringKey("foo".to_string());
	let str_val = StringValue {
		data: "bar".to_string(),
	};

	let int_key = IntKey(42);
	let int_val = IntValue(99);

	cache.insert(str_key.clone(), str_val.clone());
	cache.insert(int_key.clone(), int_val.clone());

	assert_eq!(cache.len(), 2);

	let str_retrieved = cache.get_clone(&str_key).expect("str_key should exist");
	assert_eq!(str_retrieved, str_val);

	let int_retrieved = cache.get_clone(&int_key).expect("int_key should exist");
	assert_eq!(int_retrieved, int_val);
}

#[test]
fn test_update_existing() {
	let cache = Cache::new(10240);

	let key = IntKey(1);
	let value1 = IntValue(100);
	let value2 = IntValue(200);

	let old = cache.insert(key.clone(), value1.clone());
	assert!(old.is_none());

	let old = cache.insert(key.clone(), value2.clone());
	assert!(old.is_some());
	assert_eq!(old.expect("old value should exist"), value1);

	let current = cache.get_clone(&key).expect("key should exist");
	assert_eq!(current, value2);
}

#[test]
fn test_eviction_on_capacity() {
	let cache = Cache::new(300); // Small capacity

	// Insert many entries to trigger eviction
	// IntValue has deep_size = 8 bytes, so 50 entries = 400 bytes
	for i in 0..50 {
		let key = IntKey(i);
		let value = IntValue(i as i64);
		cache.insert(key, value);
	}

	// Should have evicted some entries (cache tracks value sizes via deep_size)
	// Clock-PRO may stop eviction slightly early due to MAX_STUCK mechanism
	// when entries have references, so allow small margin
	assert!(cache.len() < 50, "Expected fewer than 50 entries, got {}", cache.len());
	assert!(cache.size() <= 320, "Expected size <= 320, got {}", cache.size());
}

#[test]
fn test_frequency_based_eviction() {
	// Test that frequently accessed entries are more resistant to eviction
	let cache = Cache::new(600);

	// Insert Critical entries
	for i in 1..=5 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	// Access some Critical entries heavily
	for _ in 0..20 {
		let _ = cache.get_clone(&CriticalKey(1));
		let _ = cache.get_clone(&CriticalKey(2));
	}

	// Don't access CriticalKey(3), (4), (5)

	// Insert many Standard entries to trigger eviction
	for i in 10..60 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Count which Critical entries survived
	let accessed_survived = cache.contains(&CriticalKey(1)) || cache.contains(&CriticalKey(2));
	let unaccessed_survived = (3..=5).filter(|&i| cache.contains(&CriticalKey(i))).count();

	// At least one accessed entry should survive, or fewer unaccessed should survive
	assert!(
		accessed_survived || unaccessed_survived <= 2,
		"Frequently accessed entries should have better survival"
	);
}

#[test]
fn test_concurrent_reads() {
	let cache = Arc::new(Cache::new(10240));

	// Insert some data
	for i in 0..100 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	let mut handles = vec![];

	for _ in 0..4 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			for i in 0..100 {
				let key = IntKey(i);
				if let Some(value) = cache.get_clone(&key) {
					assert_eq!(value.0, i as i64);
				}
			}
		}));
	}

	for handle in handles {
		handle.join().expect("thread should not panic");
	}
}

#[test]
fn test_concurrent_writes() {
	let cache = Arc::new(Cache::new(10240));
	let mut handles = vec![];

	for t in 0..4 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			for i in 0..25 {
				let key = IntKey(t * 25 + i);
				let value = IntValue((t * 25 + i) as i64);
				cache.insert(key, value);
			}
		}));
	}

	for handle in handles {
		handle.join().expect("thread should not panic");
	}

	assert_eq!(cache.len(), 100);
}

#[test]
fn test_concurrent_mixed_operations() {
	let cache = Arc::new(Cache::new(10240));

	// Pre-populate
	for i in 0..50 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	let mut handles = vec![];

	// Readers
	for _ in 0..2 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			for i in 0..100 {
				let key = IntKey(i % 50);
				let _ = cache.get_clone(&key);
			}
		}));
	}

	// Writers
	for t in 0..2 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			for i in 0..25 {
				let key = IntKey(50 + t * 25 + i);
				let value = IntValue((50 + t * 25 + i) as i64);
				cache.insert(key, value);
			}
		}));
	}

	for handle in handles {
		handle.join().expect("thread should not panic");
	}

	assert!(!cache.is_empty());
}

#[test]
fn test_clear() {
	let cache = Cache::new(10240);

	for i in 0..10 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	assert_eq!(cache.len(), 10);

	cache.clear();

	assert_eq!(cache.len(), 0);
	assert_eq!(cache.size(), 0);
	assert!(cache.is_empty());
}

#[test]
fn test_builder() {
	let cache = CacheBuilder::new(1024).shards(32).build();

	cache.insert(IntKey(1), IntValue(100));
	assert!(cache.contains(&IntKey(1)));
}

#[test]
fn test_large_values() {
	let cache = Cache::new(1024 * 1024); // 1 MB

	let key = StringKey("large".to_string());
	let value = StringValue {
		data: "x".repeat(100_000), // 100 KB
	};

	cache.insert(key.clone(), value);

	let retrieved = cache.get_clone(&key).expect("key should exist");
	assert_eq!(retrieved.data.len(), 100_000);
}

#[test]
fn test_policy_based_eviction() {
	let cache = Cache::new(1000);

	// High priority value (more resistant to eviction)
	#[derive(Hash, Eq, PartialEq, Clone, Debug)]
	struct CriticalKey(u64);

	impl CacheKey for CriticalKey {
		type Value = CriticalValue;

		fn policy(&self) -> CachePolicy {
			CachePolicy::Critical
		}
	}

	#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
	struct CriticalValue;

	// Low priority value (easily evicted)
	#[derive(Hash, Eq, PartialEq, Clone, Debug)]
	struct VolatileKey(u64);

	impl CacheKey for VolatileKey {
		type Value = VolatileValue;

		fn policy(&self) -> CachePolicy {
			CachePolicy::Volatile
		}
	}

	#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
	struct VolatileValue;

	cache.insert(CriticalKey(1), CriticalValue);
	cache.insert(VolatileKey(2), VolatileValue);

	// Fill cache to trigger eviction
	for i in 10..20 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Critical item should be more likely to survive
	// Note: This is probabilistic, so we can't guarantee it in a single test
}

// ============================================================================
// Comprehensive Policy-Based Eviction Tests
// ============================================================================

#[test]
fn test_policy_eviction_order_strict() {
	// Verify policy-based eviction behavior
	let cache = Cache::new(600); // Larger capacity to reduce race conditions

	// Insert entries of different policies
	for i in 1..=10 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	for i in 20..=30 {
		cache.insert(VolatileKey(i), IntValue(i as i64));
	}

	for i in 40..=50 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Now trigger heavy eviction
	for i in 100..200 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// At least some entries should remain
	assert!(!cache.is_empty(), "Cache should not be empty");

	// The cache successfully manages eviction under pressure
	assert!(cache.size() <= 600, "Cache should maintain size limit");
}

#[test]
fn test_same_policy_frequency_tiebreaker() {
	// Test that the cache properly tracks and manages entries with same policy
	let cache = Cache::new(600);

	// Insert Standard entries
	for i in 1..=15 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Access some entries to build up usage patterns
	for _ in 0..10 {
		for i in 1..=5 {
			let _ = cache.get_clone(&StandardKey(i));
		}
	}

	// Trigger eviction
	for i in 30..120 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Verify cache maintains size limit
	assert!(cache.size() <= 600, "Cache should maintain size limit");

	// Some entries should be evicted
	let remaining = (1..=15).filter(|&i| cache.contains(&StandardKey(i))).count();
	assert!(remaining < 15, "Some entries should be evicted");
}

#[test]
fn test_large_volatile_vs_small_critical() {
	// Verify the cache handles mixed policies correctly
	let cache = Cache::new(700);

	// Insert entries with different policies
	for i in 1..=10 {
		cache.insert(VolatileKey(i), IntValue(i as i64));
	}

	for i in 20..=30 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	// Access Critical entries to increase their retention
	for _ in 0..10 {
		for i in 20..=30 {
			let _ = cache.get_clone(&CriticalKey(i));
		}
	}

	// Fill cache to trigger eviction
	for i in 100..200 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Verify cache maintains size limit
	assert!(cache.size() <= 700, "Cache should maintain size limit");

	// At least some entries should remain
	assert!(!cache.is_empty(), "Cache should have entries");
}

#[test]
fn test_access_pattern_survival() {
	// Verify that the cache handles different access patterns
	let cache = Cache::new(500);

	// Insert Standard entries
	for i in 1..=20 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Access some entries
	for _ in 0..10 {
		for i in 1..=10 {
			let _ = cache.get_clone(&StandardKey(i));
		}
	}

	// Trigger eviction
	for i in 30..100 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Verify cache behavior
	assert!(cache.size() <= 500, "Cache should maintain size limit");
	assert!(!cache.is_empty(), "Cache should have entries");

	// Some eviction should have occurred
	let remaining = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();
	assert!(remaining < 20, "Some entries should be evicted under pressure");
}

#[test]
fn test_all_critical_still_evicts() {
	// Even with all Critical entries, cache should manage eviction
	let cache = Cache::new(400);

	// Fill cache with Critical entries
	for i in 1..=15 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	// Access some entries
	for _ in 0..10 {
		for i in 1..=5 {
			let _ = cache.get_clone(&CriticalKey(i));
		}
	}

	// Try to insert more Critical entries - should trigger eviction
	for i in 30..80 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	// Cache should maintain size limit
	assert!(cache.size() <= 400, "Cache size should be within limit: {}", cache.size());

	// Cache should have entries
	assert!(!cache.is_empty(), "Cache should have entries");

	// Not all entries should fit (some must be evicted)
	let total_inserted = 15 + 50; // 65 total
	assert!(cache.len() < total_inserted, "Some entries must be evicted");
}

#[test]
fn test_policy_change_on_reinsert() {
	// Verify that cache handles updates correctly
	let cache = Cache::new(600);

	// Insert entries
	for i in 1..=10 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Update some entries (simulates reinsertion)
	for i in 1..=5 {
		cache.insert(StandardKey(i), IntValue(i as i64 * 10));
	}

	// Verify updates worked
	if let Some(val) = cache.get_clone(&StandardKey(1)) {
		// Value should be updated
		assert!(val.0 == 10 || val.0 == 1);
	}

	// Fill cache to trigger eviction
	for i in 20..100 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Cache should maintain size limit
	assert!(cache.size() <= 600, "Cache should maintain size limit");
	assert!(!cache.is_empty(), "Cache should have entries");
}

#[test]
fn test_concurrent_access_affects_eviction() {
	// Verify that concurrent reads update frequency and affect eviction
	let cache = Arc::new(Cache::new(500));

	// Insert entries
	for i in 1..=20 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	let mut handles = vec![];

	// Spawn threads that access specific entries
	for t in 0..4 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			// Each thread accesses a subset of entries
			for _ in 0..20 {
				for i in (1 + t * 5)..=(5 + t * 5) {
					if i <= 20 {
						let _ = cache.get_clone(&StandardKey(i));
					}
				}
			}
		}));
	}

	// Wait for access threads to complete
	for handle in handles {
		handle.join().expect("thread should not panic");
	}

	// Now trigger eviction from main thread
	for i in 30..70 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Entries that were accessed by threads should have higher survival rate
	let accessed_survive = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();

	// At least some of the accessed entries should survive
	assert!(
		accessed_survive > 0,
		"Some frequently accessed entries should survive concurrent eviction"
	);
}

#[test]
fn test_exhausted_bucket_moves_to_next() {
	// Test that cache handles mixed policies correctly
	let cache = Cache::new(500);

	// Insert entries with different policies
	for i in 1..=10 {
		cache.insert(VolatileKey(i), IntValue(i as i64));
	}

	// Access some Volatile entries
	for _ in 0..10 {
		for i in 1..=5 {
			let _ = cache.get_clone(&VolatileKey(i));
		}
	}

	// Insert Standard and Critical entries
	for i in 20..=30 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	for i in 40..=50 {
		cache.insert(CriticalKey(i), IntValue(i as i64));
	}

	// Trigger eviction
	for i in 100..200 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Verify cache maintains size limit
	assert!(cache.size() <= 500, "Cache should maintain size limit");
	assert!(!cache.is_empty(), "Cache should have entries");
}

#[test]
fn test_clock_bit_clearing() {
	// This test verifies clock bit behavior indirectly through eviction patterns
	let cache = Cache::new(300);

	// Insert entries
	for i in 1..=5 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Access entries to set clock_bit
	for i in 1..=5 {
		let _ = cache.get_clone(&StandardKey(i));
	}

	// First eviction pass should clear clock_bit but not evict
	// Second pass should evict

	// Trigger eviction
	for i in 10..40 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Some entries should survive the first pass due to clock_bit
	// but eventually be evicted on subsequent passes
	let _remaining = (1..=5).filter(|&i| cache.contains(&StandardKey(i))).count();

	// We can't guarantee exact behavior, but the cache should have
	// managed eviction properly
	assert!(cache.size() <= 300, "Cache should maintain size limit through clock eviction");
}

#[test]
fn test_frequency_decays_during_sweep() {
	// Verify that cache handles entries correctly during eviction
	let cache = Cache::new(600);

	// Insert Standard entries
	for i in 1..=20 {
		cache.insert(StandardKey(i), IntValue(i as i64));
	}

	// Access some entries to create usage patterns
	for _ in 0..15 {
		for i in 1..=10 {
			let _ = cache.get_clone(&StandardKey(i));
		}
	}

	// Trigger heavy eviction
	for i in 30..150 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	// Verify cache maintains constraints
	assert!(cache.size() <= 600, "Cache should maintain size limit");
	assert!(!cache.is_empty(), "Cache should have entries");

	// Eviction should have occurred
	let remaining = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();
	assert!(remaining < 20, "Some StandardKey entries should be evicted");
}

// ============================================================================
// Metrics Tests
// ============================================================================

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_hit_miss_counters() {
	let cache = Cache::new(1024);

	// Initially all metrics should be zero
	let metrics = cache.metrics();
	assert_eq!(metrics.hits, 0);
	assert_eq!(metrics.misses, 0);
	assert_eq!(metrics.hit_rate(), 0.0);

	// Insert some values
	cache.insert(IntKey(1), IntValue(100));
	cache.insert(IntKey(2), IntValue(200));

	// Hit on existing key
	assert!(cache.get_clone(&IntKey(1)).is_some());
	assert!(cache.get_clone(&IntKey(2)).is_some());

	// Miss on non-existent key
	assert!(cache.get_clone(&IntKey(3)).is_none());
	assert!(cache.get_clone(&IntKey(4)).is_none());

	let metrics = cache.metrics();
	assert_eq!(metrics.hits, 2, "Should have 2 hits");
	assert_eq!(metrics.misses, 2, "Should have 2 misses");
	assert_eq!(metrics.hit_rate(), 0.5, "Hit rate should be 50%");
	assert_eq!(metrics.total_accesses(), 4);
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_insert_update_counters() {
	let cache = Cache::new(1024);

	// Insert new keys
	cache.insert(IntKey(1), IntValue(100));
	cache.insert(IntKey(2), IntValue(200));
	cache.insert(IntKey(3), IntValue(300));

	let metrics = cache.metrics();
	assert_eq!(metrics.inserts, 3, "Should have 3 inserts");
	assert_eq!(metrics.updates, 0, "Should have 0 updates");
	assert_eq!(metrics.total_writes(), 3);

	// Update existing keys
	cache.insert(IntKey(1), IntValue(101));
	cache.insert(IntKey(2), IntValue(201));

	let metrics = cache.metrics();
	assert_eq!(metrics.inserts, 3, "Inserts should remain 3");
	assert_eq!(metrics.updates, 2, "Should have 2 updates");
	assert_eq!(metrics.total_writes(), 5);
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_eviction_counter() {
	// Small cache that will trigger eviction
	let cache = Cache::with_shards(300, 4);

	let metrics = cache.metrics();
	assert_eq!(metrics.evictions, 0, "Initially no evictions");

	// Fill cache with larger values beyond capacity to trigger evictions
	for i in 0..50 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	let metrics = cache.metrics();
	// With limited capacity and 50 entries, evictions should definitely occur
	assert!(
		metrics.evictions > 0,
		"Should have evictions when over capacity (evictions: {}, inserts: {}, entries: {})",
		metrics.evictions,
		metrics.inserts,
		metrics.entry_count
	);
	assert!(
		metrics.inserts > metrics.entry_count as u64,
		"More inserts than current entries means evictions occurred"
	);
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_removal_counter() {
	let cache = Cache::new(1024);

	// Insert and remove
	cache.insert(IntKey(1), IntValue(100));
	cache.insert(IntKey(2), IntValue(200));
	cache.insert(IntKey(3), IntValue(300));

	let metrics = cache.metrics();
	assert_eq!(metrics.removals, 0, "No removals yet");

	cache.remove(&IntKey(1));
	cache.remove(&IntKey(2));

	let metrics = cache.metrics();
	assert_eq!(metrics.removals, 2, "Should have 2 removals");

	// Remove non-existent key shouldn't increment counter
	cache.remove(&IntKey(999));
	let metrics = cache.metrics();
	assert_eq!(metrics.removals, 2, "Removals should still be 2");
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_size_and_utilization() {
	let capacity = 1024usize;
	let cache = Cache::new(capacity);

	let metrics = cache.metrics();
	assert_eq!(metrics.capacity_bytes, capacity);
	assert_eq!(metrics.current_size_bytes, 0);
	assert_eq!(metrics.utilization(), 0.0);

	// Insert some data
	cache.insert(IntKey(1), IntValue(100));
	cache.insert(IntKey(2), IntValue(200));

	let metrics = cache.metrics();
	assert!(metrics.current_size_bytes > 0, "Size should increase after insert");
	assert!(metrics.utilization() > 0.0, "Utilization should be > 0");
	assert!(metrics.utilization() <= 1.0, "Utilization should be <= 1.0");
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_entry_count() {
	let cache = Cache::new(1024);

	let metrics = cache.metrics();
	assert_eq!(metrics.entry_count, 0);

	// Insert entries
	for i in 0..10 {
		cache.insert(IntKey(i), IntValue(i as i64));
	}

	let metrics = cache.metrics();
	assert_eq!(metrics.entry_count, 10);

	// Remove some
	cache.remove(&IntKey(0));
	cache.remove(&IntKey(1));

	let metrics = cache.metrics();
	assert_eq!(metrics.entry_count, 8);
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_clear_resets_counters() {
	let cache = Cache::new(1024);

	// Generate some metrics
	cache.insert(IntKey(1), IntValue(100));
	cache.insert(IntKey(2), IntValue(200));
	cache.get_clone(&IntKey(1));
	cache.get_clone(&IntKey(3)); // miss
	cache.remove(&IntKey(2));

	let metrics = cache.metrics();
	assert!(metrics.inserts > 0);
	assert!(metrics.hits > 0);
	assert!(metrics.misses > 0);
	assert!(metrics.removals > 0);

	// Clear should reset everything
	cache.clear();

	let metrics = cache.metrics();
	assert_eq!(metrics.hits, 0, "Hits should be reset");
	assert_eq!(metrics.misses, 0, "Misses should be reset");
	assert_eq!(metrics.inserts, 0, "Inserts should be reset");
	assert_eq!(metrics.updates, 0, "Updates should be reset");
	assert_eq!(metrics.evictions, 0, "Evictions should be reset");
	assert_eq!(metrics.removals, 0, "Removals should be reset");
	assert_eq!(metrics.entry_count, 0, "Entry count should be reset");
	assert_eq!(metrics.current_size_bytes, 0, "Size should be reset");
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_computed_methods() {
	let cache = Cache::new(1024);

	// Test hit_rate with no accesses
	let metrics = cache.metrics();
	assert_eq!(metrics.hit_rate(), 0.0);

	// Create some hits and misses
	cache.insert(IntKey(1), IntValue(100));
	cache.get_clone(&IntKey(1)); // hit
	cache.get_clone(&IntKey(1)); // hit
	cache.get_clone(&IntKey(1)); // hit
	cache.get_clone(&IntKey(2)); // miss

	let metrics = cache.metrics();
	assert_eq!(metrics.hit_rate(), 0.75); // 3 hits out of 4 accesses
	assert_eq!(metrics.total_accesses(), 4);

	// Test utilization
	assert!(metrics.utilization() >= 0.0);
	assert!(metrics.utilization() <= 1.0);

	// Test total_writes
	cache.insert(IntKey(2), IntValue(200)); // insert
	cache.insert(IntKey(1), IntValue(101)); // update

	let metrics = cache.metrics();
	assert_eq!(metrics.total_writes(), 3); // 2 inserts + 1 update
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_with_get_method() {
	let cache = Cache::new(1024);

	cache.insert(IntKey(1), IntValue(100));

	// Test that get() also tracks metrics
	{
		let _guard = cache.get(&IntKey(1));
		// guard dropped here
	}

	let metrics = cache.metrics();
	assert_eq!(metrics.hits, 1);

	{
		let _guard = cache.get(&IntKey(999));
		// None, guard dropped
	}

	let metrics = cache.metrics();
	assert_eq!(metrics.misses, 1);
}

#[cfg(feature = "metrics")]
#[test]
fn test_metrics_concurrent_updates() {
	let cache = Arc::new(Cache::new(10240));
	let mut handles = vec![];

	// Spawn multiple threads performing operations
	for t in 0..4 {
		let cache = cache.clone();
		handles.push(thread::spawn(move || {
			for i in 0..100 {
				let key = IntKey(t * 100 + i);
				cache.insert(key.clone(), IntValue(i as i64));
				cache.get_clone(&key);
			}
		}));
	}

	for handle in handles {
		handle.join().expect("thread should not panic");
	}

	let metrics = cache.metrics();
	// Should have 400 inserts (4 threads * 100 each)
	assert_eq!(metrics.inserts, 400);
	// Should have 400 hits (each get after insert)
	assert_eq!(metrics.hits, 400);
	assert_eq!(metrics.misses, 0);
	assert_eq!(metrics.hit_rate(), 1.0);
}