Skip to main content

matrixcode_core/memory/
pattern_registry.rs

1//! Pattern registry for managing conversation patterns.
2//!
3//! This module provides the central registry for storing, retrieving, and
4//! learning conversation patterns. All patterns are learned from conversations,
5//! no hardcoded presets are loaded.
6
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::Result;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15use super::conversation_pattern::{ConversationPattern, PatternType};
16use crate::constants::MATRIX_DIR;
17
18// ============================================================================
19// Configuration
20// ============================================================================
21
22/// Configuration for the pattern registry.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PatternRegistryConfig {
25    /// Maximum number of patterns to store per type.
26    pub max_patterns_per_type: usize,
27    /// Minimum confidence threshold for patterns to be active.
28    pub min_confidence_threshold: f32,
29    /// Minimum frequency for a pattern to be considered established.
30    pub min_frequency: u32,
31    /// Whether to auto-learn patterns from conversations.
32    pub auto_learn: bool,
33    /// Days before unused patterns are deactivated.
34    pub inactive_after_days: i64,
35}
36
37impl Default for PatternRegistryConfig {
38    fn default() -> Self {
39        Self {
40            max_patterns_per_type: 100,
41            min_confidence_threshold: 0.3,
42            min_frequency: 2,
43            auto_learn: true,
44            inactive_after_days: 90,
45        }
46    }
47}
48
49impl PatternRegistryConfig {
50    /// Create a config with custom max patterns.
51    pub fn with_max_patterns(max: usize) -> Self {
52        Self {
53            max_patterns_per_type: max,
54            ..Self::default()
55        }
56    }
57
58    /// Create a minimal config for low-memory environments.
59    pub fn minimal() -> Self {
60        Self {
61            max_patterns_per_type: 50,
62            min_confidence_threshold: 0.5,
63            min_frequency: 3,
64            auto_learn: true,
65            inactive_after_days: 60,
66        }
67    }
68}
69
70// ============================================================================
71// Pattern Registry
72// ============================================================================
73
74/// Central registry for conversation patterns.
75///
76/// Manages a collection of patterns organized by type, supports
77/// pattern learning, and persistence. All patterns are learned
78/// from conversations, no hardcoded presets are loaded.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PatternRegistry {
81    /// All patterns indexed by ID.
82    patterns: Vec<ConversationPattern>,
83    /// Configuration.
84    #[serde(default)]
85    config: PatternRegistryConfig,
86    /// Index for fast type-based lookup.
87    #[serde(skip)]
88    type_index: HashMap<PatternType, Vec<usize>>,
89}
90
91impl Default for PatternRegistry {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl PatternRegistry {
98    /// Create a new empty pattern registry.
99    ///
100    /// All patterns are learned from conversations, no presets are loaded.
101    pub fn new() -> Self {
102        Self {
103            patterns: Vec::new(),
104            config: PatternRegistryConfig::default(),
105            type_index: HashMap::new(),
106        }
107    }
108
109    /// Create a registry with custom configuration.
110    ///
111    /// All patterns are learned from conversations, no presets are loaded.
112    pub fn with_config(config: PatternRegistryConfig) -> Self {
113        Self {
114            patterns: Vec::new(),
115            config,
116            type_index: HashMap::new(),
117        }
118    }
119
120    /// Rebuild the type index after modifications.
121    fn rebuild_index(&mut self) {
122        self.type_index.clear();
123        for (idx, pattern) in self.patterns.iter().enumerate() {
124            self.type_index
125                .entry(pattern.pattern_type)
126                .or_default()
127                .push(idx);
128        }
129    }
130
131    /// Add a pattern internally (without rebuilding index).
132    fn add_pattern_internal(&mut self, pattern: ConversationPattern) {
133        // Check for duplicate patterns
134        if self.patterns.iter().any(|p| p.pattern == pattern.pattern) {
135            return;
136        }
137
138        // Check capacity per type
139        let type_count = self
140            .patterns
141            .iter()
142            .filter(|p| p.pattern_type == pattern.pattern_type)
143            .count();
144
145        if type_count >= self.config.max_patterns_per_type {
146            // Remove lowest frequency pattern of same type
147            if let Some(idx) = self
148                .patterns
149                .iter()
150                .enumerate()
151                .filter(|(_, p)| p.pattern_type == pattern.pattern_type)
152                .min_by_key(|(_, p)| (p.frequency, (p.confidence * 100.0) as u32))
153                .map(|(i, _)| i)
154            {
155                self.patterns.remove(idx);
156            }
157        }
158
159        self.patterns.push(pattern);
160    }
161
162    /// Add a new pattern to the registry.
163    pub fn add_pattern(&mut self, pattern: ConversationPattern) {
164        self.add_pattern_internal(pattern);
165        self.rebuild_index();
166    }
167
168    /// Add multiple patterns at once.
169    pub fn add_patterns(&mut self, patterns: Vec<ConversationPattern>) {
170        for pattern in patterns {
171            self.add_pattern_internal(pattern);
172        }
173        self.rebuild_index();
174    }
175
176    /// Learn patterns from a list of detected patterns.
177    ///
178    /// Existing patterns have their frequency incremented;
179    /// new patterns are added with initial confidence.
180    pub fn learn_patterns(&mut self, patterns: &[ConversationPattern]) {
181        for new_pattern in patterns {
182            if let Some(existing) = self
183                .patterns
184                .iter_mut()
185                .find(|p| p.pattern == new_pattern.pattern)
186            {
187                existing.mark_used();
188            } else if self.config.auto_learn {
189                self.add_pattern_internal(new_pattern.clone());
190            }
191        }
192        self.rebuild_index();
193    }
194
195    /// Get all active patterns.
196    pub fn get_active_patterns(&self) -> Vec<&ConversationPattern> {
197        self.patterns.iter().filter(|p| p.is_active).collect()
198    }
199
200    /// Get active patterns of a specific type.
201    pub fn get_active_patterns_by_type(&self, pattern_type: PatternType) -> Vec<&ConversationPattern> {
202        self.patterns
203            .iter()
204            .filter(|p| p.is_active && p.pattern_type == pattern_type)
205            .collect()
206    }
207
208    /// Get all active reference patterns.
209    pub fn get_active_reference_patterns(&self) -> Vec<String> {
210        self.get_active_patterns_by_type(PatternType::Reference)
211            .iter()
212            .map(|p| p.pattern.clone())
213            .collect()
214    }
215
216    /// Get all active code patterns.
217    pub fn get_active_code_patterns(&self) -> Vec<String> {
218        self.get_active_patterns_by_type(PatternType::Code)
219            .iter()
220            .map(|p| p.pattern.clone())
221            .collect()
222    }
223
224    /// Get pattern by ID.
225    pub fn get_pattern(&self, id: &str) -> Option<&ConversationPattern> {
226        self.patterns.iter().find(|p| p.id == id)
227    }
228
229    /// Get pattern by ID for mutation.
230    pub fn get_pattern_mut(&mut self, id: &str) -> Option<&mut ConversationPattern> {
231        self.patterns.iter_mut().find(|p| p.id == id)
232    }
233
234    /// Deactivate a pattern by ID.
235    pub fn deactivate_pattern(&mut self, id: &str) -> bool {
236        if let Some(pattern) = self.get_pattern_mut(id) {
237            pattern.deactivate();
238            true
239        } else {
240            false
241        }
242    }
243
244    /// Activate a pattern by ID.
245    pub fn activate_pattern(&mut self, id: &str) -> bool {
246        if let Some(pattern) = self.get_pattern_mut(id) {
247            pattern.activate();
248            true
249        } else {
250            false
251        }
252    }
253
254    /// Remove a pattern by ID.
255    pub fn remove_pattern(&mut self, id: &str) -> bool {
256        let len_before = self.patterns.len();
257        self.patterns.retain(|p| p.id != id);
258        if self.patterns.len() < len_before {
259            self.rebuild_index();
260            true
261        } else {
262            false
263        }
264    }
265
266    /// Get total pattern count.
267    pub fn len(&self) -> usize {
268        self.patterns.len()
269    }
270
271    /// Check if registry is empty.
272    pub fn is_empty(&self) -> bool {
273        self.patterns.is_empty()
274    }
275
276    /// Get pattern count by type.
277    pub fn count_by_type(&self, pattern_type: PatternType) -> usize {
278        self.patterns
279            .iter()
280            .filter(|p| p.pattern_type == pattern_type)
281            .count()
282    }
283
284    /// Get active pattern count by type.
285    pub fn active_count_by_type(&self, pattern_type: PatternType) -> usize {
286        self.get_active_patterns_by_type(pattern_type).len()
287    }
288
289    /// Get all patterns (for serialization).
290    pub fn all_patterns(&self) -> &[ConversationPattern] {
291        &self.patterns
292    }
293
294    /// Prune inactive patterns below confidence/frequency thresholds.
295    pub fn prune(&mut self) {
296        let now = Utc::now();
297        let threshold_days = self.config.inactive_after_days;
298
299        self.patterns.retain(|p| {
300            // Keep active patterns
301            if p.is_active {
302                return true;
303            }
304            // Keep presets
305            if p.source.is_preset() {
306                return true;
307            }
308            // Keep manually added patterns
309            if p.source.is_manual() {
310                return true;
311            }
312            // Keep if above frequency/confidence thresholds
313            if p.frequency >= self.config.min_frequency
314                && p.confidence >= self.config.min_confidence_threshold
315            {
316                return true;
317            }
318            // Remove if too old
319            let age = (now - p.last_used).num_days();
320            age < threshold_days
321        });
322
323        self.rebuild_index();
324    }
325
326    /// Get statistics about the registry.
327    pub fn stats(&self) -> PatternRegistryStats {
328        let total = self.patterns.len();
329        let active = self.patterns.iter().filter(|p| p.is_active).count();
330        let reference_count = self.count_by_type(PatternType::Reference);
331        let code_count = self.count_by_type(PatternType::Code);
332        let presets = self.patterns.iter().filter(|p| p.source.is_preset()).count();
333        let manual = self.patterns.iter().filter(|p| p.source.is_manual()).count();
334        let learned = total - presets - manual;
335
336        PatternRegistryStats {
337            total,
338            active,
339            inactive: total - active,
340            reference_count,
341            code_count,
342            presets,
343            manual,
344            learned,
345        }
346    }
347
348    // ========================================================================
349    // File Storage Methods
350    // ========================================================================
351
352    /// Get the default patterns file path (~/.matrix/patterns.json).
353    pub fn get_patterns_file_path() -> Result<PathBuf> {
354        let home = std::env::var_os("HOME")
355            .or_else(|| std::env::var_os("USERPROFILE"))
356            .ok_or_else(|| anyhow::anyhow!("HOME or USERPROFILE not set"))?;
357        Ok(PathBuf::from(home).join(MATRIX_DIR).join("patterns.json"))
358    }
359
360    /// Load patterns from a JSON file.
361    ///
362    /// If the file doesn't exist, returns an empty registry.
363    /// If the file exists but is empty or malformed, returns an empty registry.
364    pub fn from_file(path: &Path) -> Result<Self> {
365        if !path.exists() {
366            // File doesn't exist - create empty registry
367            return Ok(Self::new());
368        }
369
370        let data = fs::read_to_string(path)?;
371
372        // Handle empty file
373        if data.trim().is_empty() {
374            return Ok(Self::new());
375        }
376
377        // Try to parse JSON
378        match serde_json::from_str::<PatternRegistry>(&data) {
379            Ok(mut registry) => {
380                registry.rebuild_index();
381                Ok(registry)
382            }
383            Err(e) => {
384                // JSON parse error - log and return empty registry
385                tracing::warn!(
386                    "Failed to parse patterns file {:?}: {}. Using empty registry.",
387                    path,
388                    e
389                );
390                Ok(Self::new())
391            }
392        }
393    }
394
395    /// Load patterns from the default file path (~/.matrix/patterns.json).
396    pub fn from_default_file() -> Result<Self> {
397        let path = Self::get_patterns_file_path()?;
398        Self::from_file(&path)
399    }
400
401    /// Save patterns to a JSON file.
402    ///
403    /// Creates the parent directory if it doesn't exist.
404    /// Uses atomic write (write to temp file, then rename) for safety.
405    pub fn save_to_file(&self, path: &Path) -> Result<()> {
406        // Ensure parent directory exists
407        let parent = path.parent();
408        if let Some(dir) = parent {
409            if !dir.exists() {
410                fs::create_dir_all(dir)?;
411            }
412        }
413
414        // Serialize to JSON
415        let json = serde_json::to_string_pretty(self)?;
416
417        // Atomic write: write to temp file first
418        let tmp_path = path.with_extension("json.tmp");
419        fs::write(&tmp_path, json)?;
420
421        // Rename temp file to final path (atomic on most systems)
422        fs::rename(&tmp_path, path)?;
423
424        Ok(())
425    }
426
427    /// Save patterns to the default file path (~/.matrix/patterns.json).
428    pub fn save_to_default_file(&self) -> Result<()> {
429        let path = Self::get_patterns_file_path()?;
430        self.save_to_file(&path)
431    }
432}
433
434// ============================================================================
435// Statistics
436// ============================================================================
437
438/// Statistics about the pattern registry.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct PatternRegistryStats {
441    /// Total pattern count.
442    pub total: usize,
443    /// Active pattern count.
444    pub active: usize,
445    /// Inactive pattern count.
446    pub inactive: usize,
447    /// Reference pattern count.
448    pub reference_count: usize,
449    /// Code pattern count.
450    pub code_count: usize,
451    /// Preset pattern count.
452    pub presets: usize,
453    /// Manually added pattern count.
454    pub manual: usize,
455    /// Learned pattern count.
456    pub learned: usize,
457}
458
459impl std::fmt::Display for PatternRegistryStats {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        writeln!(f, "Pattern Registry Stats:")?;
462        writeln!(f, "  Total: {} (active: {}, inactive: {})", self.total, self.active, self.inactive)?;
463        writeln!(f, "  Reference: {}, Code: {}", self.reference_count, self.code_count)?;
464        writeln!(f, "  Presets: {}, Manual: {}, Learned: {}", self.presets, self.manual, self.learned)
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use crate::memory::conversation_pattern::PatternSource;
472
473    // =========================================================================
474    // PatternRegistryConfig Tests
475    // =========================================================================
476
477    #[test]
478    fn test_config_default() {
479        let config = PatternRegistryConfig::default();
480
481        assert_eq!(config.max_patterns_per_type, 100);
482        assert_eq!(config.min_confidence_threshold, 0.3);
483        assert_eq!(config.min_frequency, 2);
484        assert!(config.auto_learn);
485        assert_eq!(config.inactive_after_days, 90);
486    }
487
488    #[test]
489    fn test_config_with_max_patterns() {
490        let config = PatternRegistryConfig::with_max_patterns(50);
491
492        assert_eq!(config.max_patterns_per_type, 50);
493        // Other fields should be default
494        assert_eq!(config.min_confidence_threshold, 0.3);
495        assert_eq!(config.min_frequency, 2);
496    }
497
498    #[test]
499    fn test_config_minimal() {
500        let config = PatternRegistryConfig::minimal();
501
502        assert_eq!(config.max_patterns_per_type, 50);
503        assert_eq!(config.min_confidence_threshold, 0.5);
504        assert_eq!(config.min_frequency, 3);
505        assert!(config.auto_learn);
506        assert_eq!(config.inactive_after_days, 60);
507    }
508
509    #[test]
510    fn test_config_serialization() {
511        let config = PatternRegistryConfig::with_max_patterns(75);
512        let json = serde_json::to_string(&config).unwrap();
513        let decoded: PatternRegistryConfig = serde_json::from_str(&json).unwrap();
514
515        assert_eq!(decoded.max_patterns_per_type, 75);
516        assert_eq!(decoded.min_confidence_threshold, config.min_confidence_threshold);
517    }
518
519    // =========================================================================
520    // PatternRegistry Creation Tests
521    // =========================================================================
522
523    #[test]
524    fn test_registry_creation() {
525        let registry = PatternRegistry::new();
526        // Should be empty - no presets loaded
527        assert!(registry.is_empty());
528        assert_eq!(registry.count_by_type(PatternType::Reference), 0);
529        assert_eq!(registry.count_by_type(PatternType::Code), 0);
530    }
531
532    #[test]
533    fn test_registry_default() {
534        let registry = PatternRegistry::default();
535        assert!(registry.is_empty());
536    }
537
538    #[test]
539    fn test_registry_with_config() {
540        let config = PatternRegistryConfig::minimal();
541        let registry = PatternRegistry::with_config(config);
542
543        assert_eq!(registry.config.max_patterns_per_type, 50);
544        assert!(registry.is_empty()); // No presets loaded
545    }
546
547    // =========================================================================
548    // Add Pattern Tests
549    // =========================================================================
550
551    #[test]
552    fn test_add_pattern() {
553        let mut registry = PatternRegistry::new();
554        assert!(registry.is_empty());
555
556        let pattern = ConversationPattern::new(
557            PatternType::Reference,
558            r"test-pattern-\d+",
559            PatternSource::Manual,
560        );
561        registry.add_pattern(pattern);
562
563        assert_eq!(registry.len(), 1);
564    }
565
566    #[test]
567    fn test_add_patterns_batch() {
568        let mut registry = PatternRegistry::new();
569        assert!(registry.is_empty());
570
571        let patterns = vec![
572            ConversationPattern::new(PatternType::Reference, "batch-1", PatternSource::Manual),
573            ConversationPattern::new(PatternType::Code, "batch-2", PatternSource::Manual),
574            ConversationPattern::new(PatternType::Reference, "batch-3", PatternSource::Manual),
575        ];
576
577        registry.add_patterns(patterns);
578
579        assert_eq!(registry.len(), 3);
580    }
581
582    #[test]
583    fn test_duplicate_prevention() {
584        let mut registry = PatternRegistry::new();
585        assert!(registry.is_empty());
586
587        let pattern1 = ConversationPattern::new(
588            PatternType::Reference,
589            "duplicate-test",
590            PatternSource::Manual,
591        );
592        let pattern2 = ConversationPattern::new(
593            PatternType::Reference,
594            "duplicate-test",
595            PatternSource::user_conversation("test"),
596        );
597
598        registry.add_pattern(pattern1);
599        registry.add_pattern(pattern2);
600
601        assert_eq!(registry.len(), 1);
602    }
603
604    #[test]
605    fn test_duplicate_prevention_in_batch() {
606        let mut registry = PatternRegistry::new();
607        assert!(registry.is_empty());
608
609        let patterns = vec![
610            ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::Manual),
611            ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::user_conversation("test")),
612        ];
613
614        registry.add_patterns(patterns);
615
616        // Only one should be added
617        assert_eq!(registry.len(), 1);
618    }
619
620    #[test]
621    fn test_capacity_limit_removes_lowest_frequency() {
622        let config = PatternRegistryConfig::with_max_patterns(2);
623        let mut registry = PatternRegistry::with_config(config);
624        assert!(registry.is_empty());
625
626        // Add patterns with different frequencies
627        let mut p1 = ConversationPattern::new(PatternType::Reference, "pattern-1", PatternSource::Manual);
628        p1.frequency = 10;
629
630        let mut p2 = ConversationPattern::new(PatternType::Reference, "pattern-2", PatternSource::Manual);
631        p2.frequency = 5;
632
633        let p3 = ConversationPattern::new(PatternType::Reference, "pattern-3", PatternSource::Manual);
634
635        registry.add_pattern(p1);
636        registry.add_pattern(p2);
637        registry.add_pattern(p3); // This should trigger removal of p2 (lowest frequency)
638
639        // pattern-2 should have been removed
640        assert!(registry.get_pattern_by_pattern("pattern-1").is_some());
641        assert!(registry.get_pattern_by_pattern("pattern-3").is_some());
642    }
643
644    // =========================================================================
645    // Get Pattern Tests
646    // =========================================================================
647
648    #[test]
649    fn test_get_active_patterns_empty() {
650        let registry = PatternRegistry::new();
651
652        let refs = registry.get_active_reference_patterns();
653        let codes = registry.get_active_code_patterns();
654
655        assert!(refs.is_empty());
656        assert!(codes.is_empty());
657    }
658
659    #[test]
660    fn test_get_active_patterns_by_type_empty() {
661        let registry = PatternRegistry::new();
662
663        let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
664        let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
665
666        assert!(ref_patterns.is_empty());
667        assert!(code_patterns.is_empty());
668    }
669
670    #[test]
671    fn test_get_active_patterns_with_added_patterns() {
672        let mut registry = PatternRegistry::new();
673
674        registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-pattern"));
675        registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-pattern"));
676
677        let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
678        let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
679
680        assert_eq!(ref_patterns.len(), 1);
681        assert_eq!(code_patterns.len(), 1);
682
683        // All returned patterns should be active and of correct type
684        for p in &ref_patterns {
685            assert!(p.is_active);
686            assert_eq!(p.pattern_type, PatternType::Reference);
687        }
688        for p in &code_patterns {
689            assert!(p.is_active);
690            assert_eq!(p.pattern_type, PatternType::Code);
691        }
692    }
693
694    #[test]
695    fn test_get_pattern_by_id() {
696        let mut registry = PatternRegistry::new();
697
698        let pattern = ConversationPattern::manual(PatternType::Code, "test-get-by-id");
699        let id = pattern.id.clone();
700        registry.add_pattern(pattern);
701
702        let found = registry.get_pattern(&id);
703        assert!(found.is_some());
704        assert_eq!(found.unwrap().pattern, "test-get-by-id");
705    }
706
707    #[test]
708    fn test_get_pattern_by_id_not_found() {
709        let registry = PatternRegistry::new();
710
711        let found = registry.get_pattern("nonexistent-id");
712        assert!(found.is_none());
713    }
714
715    #[test]
716    fn test_get_pattern_mut() {
717        let mut registry = PatternRegistry::new();
718
719        let pattern = ConversationPattern::manual(PatternType::Code, "test-get-mut");
720        let id = pattern.id.clone();
721        registry.add_pattern(pattern);
722
723        let found = registry.get_pattern_mut(&id);
724        assert!(found.is_some());
725        found.unwrap().frequency = 100;
726
727        let found_again = registry.get_pattern(&id).unwrap();
728        assert_eq!(found_again.frequency, 100);
729    }
730
731    #[test]
732    fn test_all_patterns_empty() {
733        let registry = PatternRegistry::new();
734        let patterns = registry.all_patterns();
735
736        assert!(patterns.is_empty());
737        assert_eq!(patterns.len(), registry.len());
738    }
739
740    #[test]
741    fn test_all_patterns_with_added() {
742        let mut registry = PatternRegistry::new();
743        registry.add_pattern(ConversationPattern::manual(PatternType::Code, "test-pattern"));
744
745        let patterns = registry.all_patterns();
746        assert_eq!(patterns.len(), 1);
747    }
748
749    // =========================================================================
750    // Pattern Activation Tests
751    // =========================================================================
752
753    #[test]
754    fn test_deactivate_pattern() {
755        let mut registry = PatternRegistry::new();
756
757        // Add a pattern
758        let pattern = ConversationPattern::manual(PatternType::Code, "test-deactivate");
759        let id = pattern.id.clone();
760        registry.add_pattern(pattern);
761
762        // Deactivate it
763        assert!(registry.deactivate_pattern(&id));
764        let p = registry.get_pattern(&id).unwrap();
765        assert!(!p.is_active);
766    }
767
768    #[test]
769    fn test_deactivate_pattern_not_found() {
770        let mut registry = PatternRegistry::new();
771
772        assert!(!registry.deactivate_pattern("nonexistent-id"));
773    }
774
775    #[test]
776    fn test_activate_pattern() {
777        let mut registry = PatternRegistry::new();
778
779        let pattern = ConversationPattern::manual(PatternType::Code, "test-activate");
780        let id = pattern.id.clone();
781        registry.add_pattern(pattern);
782
783        // First deactivate
784        registry.deactivate_pattern(&id);
785        assert!(!registry.get_pattern(&id).unwrap().is_active);
786
787        // Then activate again
788        assert!(registry.activate_pattern(&id));
789        assert!(registry.get_pattern(&id).unwrap().is_active);
790    }
791
792    #[test]
793    fn test_activate_pattern_not_found() {
794        let mut registry = PatternRegistry::new();
795
796        assert!(!registry.activate_pattern("nonexistent-id"));
797    }
798
799    #[test]
800    fn test_active_count_by_type() {
801        let mut registry = PatternRegistry::new();
802        let initial_active = registry.active_count_by_type(PatternType::Code);
803
804        // Add and deactivate a pattern
805        let pattern = ConversationPattern::manual(PatternType::Code, "test-count");
806        let id = pattern.id.clone();
807        registry.add_pattern(pattern);
808
809        assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active + 1);
810
811        registry.deactivate_pattern(&id);
812        assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
813    }
814
815    // =========================================================================
816    // Remove Pattern Tests
817    // =========================================================================
818
819    #[test]
820    fn test_remove_pattern() {
821        let mut registry = PatternRegistry::new();
822
823        let pattern = ConversationPattern::manual(PatternType::Code, "test-remove");
824        let id = pattern.id.clone();
825        registry.add_pattern(pattern);
826
827        let len_before = registry.len();
828        assert!(registry.remove_pattern(&id));
829        assert_eq!(registry.len(), len_before - 1);
830        assert!(registry.get_pattern(&id).is_none());
831    }
832
833    #[test]
834    fn test_remove_pattern_not_found() {
835        let mut registry = PatternRegistry::new();
836
837        assert!(!registry.remove_pattern("nonexistent-id"));
838    }
839
840    // =========================================================================
841    // Learn Patterns Tests
842    // =========================================================================
843
844    #[test]
845    fn test_learn_patterns() {
846        let mut registry = PatternRegistry::new();
847        let initial_ref_count = registry.count_by_type(PatternType::Reference);
848
849        // Learn a new pattern
850        let new_pattern =
851            ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
852        registry.learn_patterns(&[new_pattern]);
853
854        assert_eq!(
855            registry.count_by_type(PatternType::Reference),
856            initial_ref_count + 1
857        );
858
859        // Learn the same pattern again (should increment frequency)
860        let same_pattern =
861            ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
862        registry.learn_patterns(&[same_pattern]);
863
864        let p = registry.patterns.iter().find(|p| p.pattern == "LEARN-123").unwrap();
865        assert_eq!(p.frequency, 2); // 1 (initial from new) + 1 (mark_used from second learn)
866    }
867
868    #[test]
869    fn test_learn_patterns_multiple_new() {
870        let mut registry = PatternRegistry::new();
871        let initial_count = registry.len();
872
873        let patterns = vec![
874            ConversationPattern::new(PatternType::Reference, "learn-1", PatternSource::Manual),
875            ConversationPattern::new(PatternType::Code, "learn-2", PatternSource::Manual),
876        ];
877
878        registry.learn_patterns(&patterns);
879
880        assert_eq!(registry.len(), initial_count + 2);
881    }
882
883    #[test]
884    fn test_learn_patterns_empty() {
885        let mut registry = PatternRegistry::new();
886        let initial_count = registry.len();
887
888        registry.learn_patterns(&[]);
889
890        assert_eq!(registry.len(), initial_count);
891    }
892
893    // =========================================================================
894    // Statistics Tests
895    // =========================================================================
896
897    #[test]
898    fn test_stats_empty() {
899        let registry = PatternRegistry::new();
900        let stats = registry.stats();
901
902        assert_eq!(stats.total, 0);
903        assert_eq!(stats.presets, 0);
904        assert_eq!(stats.active, 0);
905        assert_eq!(stats.inactive, 0);
906        assert_eq!(stats.reference_count, 0);
907        assert_eq!(stats.code_count, 0);
908    }
909
910    #[test]
911    fn test_stats_with_manual_patterns() {
912        let mut registry = PatternRegistry::new();
913
914        registry.add_pattern(ConversationPattern::manual(PatternType::Code, "manual-1"));
915        registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "manual-2"));
916
917        let stats = registry.stats();
918        assert_eq!(stats.total, 2);
919        assert_eq!(stats.manual, 2);
920        assert_eq!(stats.presets, 0);
921    }
922
923    #[test]
924    fn test_stats_display() {
925        let registry = PatternRegistry::new();
926        let stats = registry.stats();
927        let display = format!("{}", stats);
928
929        assert!(display.contains("Pattern Registry Stats"));
930        assert!(display.contains("Total:"));
931        assert!(display.contains("active:"));
932        assert!(display.contains("Reference:"));
933        assert!(display.contains("Code:"));
934    }
935
936    // =========================================================================
937    // Serialization Tests
938    // =========================================================================
939
940    #[test]
941    fn test_serialization() {
942        let registry = PatternRegistry::new();
943        let json = serde_json::to_string(&registry).unwrap();
944        let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
945
946        assert_eq!(decoded.len(), registry.len());
947        assert_eq!(
948            decoded.count_by_type(PatternType::Reference),
949            registry.count_by_type(PatternType::Reference)
950        );
951    }
952
953    #[test]
954    fn test_serialization_preserves_patterns() {
955        let mut registry = PatternRegistry::new();
956
957        let pattern = ConversationPattern::manual(PatternType::Code, "serialize-test")
958            .with_description("Test serialization")
959            .with_tag("test");
960        let pattern_id = pattern.id.clone();
961        registry.add_pattern(pattern);
962
963        let json = serde_json::to_string(&registry).unwrap();
964        let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
965
966        let found = decoded.get_pattern(&pattern_id).unwrap();
967        assert_eq!(found.pattern, "serialize-test");
968        assert_eq!(found.description, Some("Test serialization".to_string()));
969        assert_eq!(found.tags, vec!["test"]);
970    }
971
972    #[test]
973    fn test_serialization_config_preserved() {
974        let config = PatternRegistryConfig::minimal();
975        let registry = PatternRegistry::with_config(config);
976
977        let json = serde_json::to_string(&registry).unwrap();
978        let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
979
980        assert_eq!(decoded.config.max_patterns_per_type, 50);
981        assert_eq!(decoded.config.min_confidence_threshold, 0.5);
982    }
983
984    // =========================================================================
985    // Prune Tests
986    // =========================================================================
987
988    #[test]
989    fn test_prune_keeps_active_patterns() {
990        let mut registry = PatternRegistry::new();
991
992        let pattern = ConversationPattern::manual(PatternType::Code, "active-pattern");
993        registry.add_pattern(pattern);
994
995        let len_before = registry.len();
996        registry.prune();
997        let len_after = registry.len();
998
999        // Active patterns should be kept
1000        assert_eq!(len_after, len_before);
1001    }
1002
1003    #[test]
1004    fn test_prune_keeps_manual_patterns() {
1005        let mut registry = PatternRegistry::new();
1006
1007        let pattern = ConversationPattern::manual(PatternType::Code, "manual-pattern");
1008        registry.add_pattern(pattern);
1009
1010        let manual_count_before = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1011        registry.prune();
1012        let manual_count_after = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1013
1014        assert_eq!(manual_count_after, manual_count_before);
1015    }
1016
1017    // =========================================================================
1018    // Length and Empty Tests
1019    // =========================================================================
1020
1021    #[test]
1022    fn test_len_and_is_empty() {
1023        let registry = PatternRegistry::new();
1024        assert!(registry.is_empty());
1025        assert_eq!(registry.len(), 0);
1026    }
1027
1028    #[test]
1029    fn test_count_by_type_empty() {
1030        let registry = PatternRegistry::new();
1031
1032        let ref_count = registry.count_by_type(PatternType::Reference);
1033        let code_count = registry.count_by_type(PatternType::Code);
1034
1035        assert_eq!(ref_count, 0);
1036        assert_eq!(code_count, 0);
1037        assert_eq!(ref_count + code_count, registry.len());
1038    }
1039
1040    #[test]
1041    fn test_count_by_type_with_patterns() {
1042        let mut registry = PatternRegistry::new();
1043        registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-1"));
1044        registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-1"));
1045
1046        assert_eq!(registry.count_by_type(PatternType::Reference), 1);
1047        assert_eq!(registry.count_by_type(PatternType::Code), 1);
1048    }
1049
1050    // =========================================================================
1051    // Edge Cases and Boundary Tests
1052    // =========================================================================
1053
1054    #[test]
1055    fn test_config_custom() {
1056        let config = PatternRegistryConfig::with_max_patterns(50);
1057        let registry = PatternRegistry::with_config(config);
1058
1059        assert_eq!(registry.config.max_patterns_per_type, 50);
1060    }
1061
1062    #[test]
1063    fn test_add_pattern_same_pattern_different_types() {
1064        let mut registry = PatternRegistry::new();
1065        assert!(registry.is_empty());
1066
1067        // Same pattern string but different types
1068        // Note: The current implementation checks duplicates by pattern string only,
1069        // so the second pattern with the same string won't be added regardless of type
1070        let p1 = ConversationPattern::new(PatternType::Reference, "same-text", PatternSource::Manual);
1071        let p2 = ConversationPattern::new(PatternType::Code, "same-text", PatternSource::Manual);
1072
1073        registry.add_pattern(p1);
1074        registry.add_pattern(p2);
1075
1076        // Only one should be added since duplicates are detected by pattern string
1077        assert_eq!(registry.len(), 1);
1078        // The first pattern (Reference type) should be the one added
1079        assert!(registry.all_patterns().iter().any(|p| p.pattern == "same-text" && p.pattern_type == PatternType::Reference));
1080    }
1081
1082    #[test]
1083    fn test_get_active_patterns_includes_only_active() {
1084        let mut registry = PatternRegistry::new();
1085
1086        let active_pattern = ConversationPattern::manual(PatternType::Code, "active-test");
1087        let active_id = active_pattern.id.clone();
1088
1089        let mut inactive_pattern = ConversationPattern::manual(PatternType::Code, "inactive-test");
1090        inactive_pattern.deactivate();
1091        let inactive_id = inactive_pattern.id.clone();
1092
1093        registry.add_pattern(active_pattern);
1094        registry.add_pattern(inactive_pattern);
1095
1096        let active_patterns = registry.get_active_patterns_by_type(PatternType::Code);
1097
1098        assert!(active_patterns.iter().any(|p| p.id == active_id));
1099        assert!(!active_patterns.iter().any(|p| p.id == inactive_id));
1100    }
1101
1102    #[test]
1103    fn test_type_index_rebuild_on_remove() {
1104        let mut registry = PatternRegistry::new();
1105
1106        let pattern = ConversationPattern::manual(PatternType::Code, "test-index");
1107        let id = pattern.id.clone();
1108        registry.add_pattern(pattern);
1109
1110        // Verify pattern exists
1111        assert!(registry.get_pattern(&id).is_some());
1112
1113        // Remove and verify index is updated
1114        registry.remove_pattern(&id);
1115        assert!(registry.get_pattern(&id).is_none());
1116
1117        // Index should still be valid for other operations
1118        let active = registry.get_active_patterns_by_type(PatternType::Code);
1119        assert!(!active.iter().any(|p| p.id == id));
1120    }
1121
1122    #[test]
1123    fn test_deactivate_then_activate_affects_active_count() {
1124        let mut registry = PatternRegistry::new();
1125
1126        let pattern = ConversationPattern::manual(PatternType::Code, "toggle-test");
1127        let id = pattern.id.clone();
1128        registry.add_pattern(pattern);
1129
1130        let initial_active = registry.active_count_by_type(PatternType::Code);
1131
1132        registry.deactivate_pattern(&id);
1133        assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active - 1);
1134
1135        registry.activate_pattern(&id);
1136        assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
1137    }
1138
1139    #[test]
1140    fn test_pattern_order_preserved_after_multiple_operations() {
1141        let mut registry = PatternRegistry::new();
1142
1143        let p1 = ConversationPattern::manual(PatternType::Reference, "order-1");
1144        let p2 = ConversationPattern::manual(PatternType::Code, "order-2");
1145        let p3 = ConversationPattern::manual(PatternType::Reference, "order-3");
1146
1147        registry.add_patterns(vec![p1, p2, p3]);
1148
1149        // All should be present
1150        assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-1"));
1151        assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-2"));
1152        assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-3"));
1153    }
1154
1155    // =========================================================================
1156    // File Storage Tests
1157    // =========================================================================
1158
1159    #[test]
1160    fn test_get_patterns_file_path() {
1161        let path = PatternRegistry::get_patterns_file_path();
1162        assert!(path.is_ok());
1163
1164        let path = path.unwrap();
1165        // Should end with .matrix/patterns.json
1166        assert!(path.to_string_lossy().contains(".matrix"));
1167        assert!(path.to_string_lossy().contains("patterns.json"));
1168    }
1169
1170    #[test]
1171    fn test_from_file_nonexistent() {
1172        let temp_dir = tempfile::tempdir().unwrap();
1173        let nonexistent_path = temp_dir.path().join("nonexistent_patterns.json");
1174
1175        let registry = PatternRegistry::from_file(&nonexistent_path).unwrap();
1176
1177        // Should be empty - no presets loaded
1178        assert!(registry.is_empty());
1179        assert_eq!(registry.count_by_type(PatternType::Reference), 0);
1180        assert_eq!(registry.count_by_type(PatternType::Code), 0);
1181    }
1182
1183    #[test]
1184    fn test_from_file_empty_file() {
1185        let temp_dir = tempfile::tempdir().unwrap();
1186        let empty_path = temp_dir.path().join("empty_patterns.json");
1187
1188        // Create empty file
1189        fs::write(&empty_path, "").unwrap();
1190
1191        let registry = PatternRegistry::from_file(&empty_path).unwrap();
1192
1193        // Should be empty - no presets loaded
1194        assert!(registry.is_empty());
1195    }
1196
1197    #[test]
1198    fn test_from_file_valid_json() {
1199        let temp_dir = tempfile::tempdir().unwrap();
1200        let file_path = temp_dir.path().join("valid_patterns.json");
1201
1202        // Create a registry and save it
1203        let mut original = PatternRegistry::new();
1204        original.add_pattern(
1205            ConversationPattern::manual(PatternType::Code, "custom-pattern")
1206                .with_description("Custom test pattern"),
1207        );
1208        original.save_to_file(&file_path).unwrap();
1209
1210        // Load it back
1211        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1212
1213        // Should have the custom pattern
1214        assert!(loaded.patterns.iter().any(|p| p.pattern == "custom-pattern"));
1215        assert!(loaded.patterns.iter().any(|p| p.description == Some("Custom test pattern".to_string())));
1216    }
1217
1218    #[test]
1219    fn test_from_file_malformed_json() {
1220        let temp_dir = tempfile::tempdir().unwrap();
1221        let malformed_path = temp_dir.path().join("malformed_patterns.json");
1222
1223        // Create file with malformed JSON
1224        fs::write(&malformed_path, "{ not valid json }").unwrap();
1225
1226        // Should return empty registry (logged warning)
1227        let registry = PatternRegistry::from_file(&malformed_path).unwrap();
1228
1229        assert!(registry.is_empty());
1230    }
1231
1232    #[test]
1233    fn test_save_to_file_creates_directory() {
1234        let temp_dir = tempfile::tempdir().unwrap();
1235        let nested_path = temp_dir.path().join("nested").join("dir").join("patterns.json");
1236
1237        // Nested directory doesn't exist
1238        assert!(!nested_path.parent().unwrap().exists());
1239
1240        let registry = PatternRegistry::new();
1241        registry.save_to_file(&nested_path).unwrap();
1242
1243        // Directory and file should now exist
1244        assert!(nested_path.parent().unwrap().exists());
1245        assert!(nested_path.exists());
1246    }
1247
1248    #[test]
1249    fn test_save_to_file_roundtrip() {
1250        let temp_dir = tempfile::tempdir().unwrap();
1251        let file_path = temp_dir.path().join("roundtrip_patterns.json");
1252
1253        // Create registry with custom patterns
1254        let mut original = PatternRegistry::new();
1255        let p1 = ConversationPattern::manual(PatternType::Reference, "roundtrip-ref")
1256            .with_description("Reference pattern for roundtrip test")
1257            .with_tag("test");
1258        let p1_id = p1.id.clone();
1259        original.add_pattern(p1);
1260
1261        let p2 = ConversationPattern::manual(PatternType::Code, "roundtrip-code")
1262            .with_description("Code pattern for roundtrip test");
1263        let p2_id = p2.id.clone();
1264        original.add_pattern(p2);
1265
1266        // Save
1267        original.save_to_file(&file_path).unwrap();
1268
1269        // Load
1270        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1271
1272        // Verify patterns preserved
1273        let loaded_p1 = loaded.get_pattern(&p1_id).unwrap();
1274        assert_eq!(loaded_p1.pattern, "roundtrip-ref");
1275        assert_eq!(loaded_p1.pattern_type, PatternType::Reference);
1276        assert_eq!(loaded_p1.description, Some("Reference pattern for roundtrip test".to_string()));
1277        assert_eq!(loaded_p1.tags, vec!["test"]);
1278
1279        let loaded_p2 = loaded.get_pattern(&p2_id).unwrap();
1280        assert_eq!(loaded_p2.pattern, "roundtrip-code");
1281        assert_eq!(loaded_p2.pattern_type, PatternType::Code);
1282    }
1283
1284    #[test]
1285    fn test_save_to_file_preserves_config() {
1286        let temp_dir = tempfile::tempdir().unwrap();
1287        let file_path = temp_dir.path().join("config_patterns.json");
1288
1289        // Create registry with custom config
1290        let config = PatternRegistryConfig::minimal();
1291        let original = PatternRegistry::with_config(config);
1292
1293        original.save_to_file(&file_path).unwrap();
1294
1295        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1296
1297        assert_eq!(loaded.config.max_patterns_per_type, 50);
1298        assert_eq!(loaded.config.min_confidence_threshold, 0.5);
1299        assert_eq!(loaded.config.min_frequency, 3);
1300    }
1301
1302    #[test]
1303    fn test_from_default_file() {
1304        // This test verifies the method works
1305        // The default file may or may not exist, so we just verify the method doesn't error
1306        let result = PatternRegistry::from_default_file();
1307        assert!(result.is_ok());
1308    }
1309
1310    #[test]
1311    fn test_file_storage_integration() {
1312        let temp_dir = tempfile::tempdir().unwrap();
1313        let file_path = temp_dir.path().join("integration_patterns.json");
1314
1315        // Start with empty registry
1316        let mut registry = PatternRegistry::from_file(&file_path).unwrap();
1317        assert!(registry.is_empty());
1318
1319        // Add custom patterns
1320        let custom = ConversationPattern::manual(PatternType::Code, "integration-test")
1321            .with_description("Integration test pattern")
1322            .with_tag("integration");
1323        registry.add_pattern(custom);
1324
1325        // Save
1326        registry.save_to_file(&file_path).unwrap();
1327
1328        // Load again
1329        let reloaded = PatternRegistry::from_file(&file_path).unwrap();
1330
1331        // Custom pattern should be present
1332        assert!(reloaded.patterns.iter().any(|p| p.pattern == "integration-test"));
1333        assert!(reloaded.patterns.iter().any(|p| p.tags.contains(&"integration".to_string())));
1334    }
1335
1336    // =========================================================================
1337    // Boundary Tests for File Storage
1338    // =========================================================================
1339
1340    #[test]
1341    fn test_large_patterns_save_and_load() {
1342        let temp_dir = tempfile::tempdir().unwrap();
1343        let file_path = temp_dir.path().join("large_patterns.json");
1344
1345        // Create empty registry and add patterns
1346        let mut registry = PatternRegistry::new();
1347
1348        // Add 50 patterns of each type
1349        for i in 0..50 {
1350            let ref_pattern = ConversationPattern::manual(PatternType::Reference, &format!("large-ref-{}", i))
1351                .with_description(&format!("Reference pattern {}", i))
1352                .with_tag(&format!("tag{}", i % 5));
1353            registry.add_pattern(ref_pattern);
1354
1355            let code_pattern = ConversationPattern::manual(PatternType::Code, &format!("large-code-{}", i))
1356                .with_description(&format!("Code pattern {}", i))
1357                .with_tag(&format!("codetag{}", i % 3));
1358            registry.add_pattern(code_pattern);
1359        }
1360
1361        let expected_count = 100;
1362        assert_eq!(registry.len(), expected_count);
1363
1364        // Save
1365        registry.save_to_file(&file_path).unwrap();
1366
1367        // Verify file was created and has reasonable size
1368        let metadata = fs::metadata(&file_path).unwrap();
1369        assert!(metadata.len() > 1000); // Should be substantial
1370
1371        // Load
1372        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1373
1374        // Verify all patterns preserved
1375        assert_eq!(loaded.len(), expected_count);
1376
1377        // Verify some specific patterns
1378        for i in 0..50 {
1379            assert!(
1380                loaded.patterns.iter().any(|p| p.pattern == format!("large-ref-{}", i)),
1381                "Missing large-ref-{}",
1382                i
1383            );
1384            assert!(
1385                loaded.patterns.iter().any(|p| p.pattern == format!("large-code-{}", i)),
1386                "Missing large-code-{}",
1387                i
1388            );
1389        }
1390    }
1391
1392    #[test]
1393    fn test_special_characters_in_patterns() {
1394        let temp_dir = tempfile::tempdir().unwrap();
1395        let file_path = temp_dir.path().join("special_chars_patterns.json");
1396
1397        // Test various special characters
1398        let special_patterns = vec![
1399            // Unicode characters
1400            ("unicode-你好", "Chinese characters"),
1401            ("unicode-日本語", "Japanese characters"),
1402            ("unicode-한국어", "Korean characters"),
1403            ("unicode-مرحبا", "Arabic characters"),
1404            ("unicode-🎉🔥", "Emoji"),
1405            // Regex special characters (should be preserved literally)
1406            ("regex-\\d+\\.\\w*", "Regex pattern with escapes"),
1407            ("regex-[a-z]+", "Regex character class"),
1408            ("regex-(foo|bar)", "Regex alternation"),
1409            // Special JSON characters
1410            ("json-\"quotes\"", "Contains quotes"),
1411            ("json-\\n\\t", "Escape sequences"),
1412            ("json-日本語\"test\"", "Mixed special chars"),
1413            // Whitespace and control characters
1414            ("whitespace-tab\ttab", "Contains tab"),
1415            ("whitespace-newline\nline", "Contains newline"),
1416        ];
1417
1418        let mut registry = PatternRegistry::new();
1419        let mut added_ids = Vec::new();
1420
1421        for (pattern_str, desc) in &special_patterns {
1422            let pattern = ConversationPattern::manual(PatternType::Code, *pattern_str)
1423                .with_description(*desc);
1424            let id = pattern.id.clone();
1425            registry.add_pattern(pattern);
1426            added_ids.push((id, *pattern_str, *desc));
1427        }
1428
1429        // Save
1430        registry.save_to_file(&file_path).unwrap();
1431
1432        // Load
1433        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1434
1435        // Verify all special patterns preserved
1436        for (id, pattern_str, desc) in &added_ids {
1437            let found = loaded.get_pattern(id);
1438            assert!(found.is_some(), "Pattern {} not found after reload", pattern_str);
1439            let p = found.unwrap();
1440            assert_eq!(p.pattern, *pattern_str, "Pattern mismatch for {}", pattern_str);
1441            assert_eq!(p.description, Some(desc.to_string()), "Description mismatch for {}", pattern_str);
1442        }
1443    }
1444
1445    #[test]
1446    fn test_empty_registry_save_and_load() {
1447        let temp_dir = tempfile::tempdir().unwrap();
1448        let file_path = temp_dir.path().join("empty_registry.json");
1449
1450        // Create empty registry
1451        let registry = PatternRegistry::new();
1452        assert!(registry.is_empty());
1453
1454        // Save empty registry
1455        registry.save_to_file(&file_path).unwrap();
1456
1457        // Load
1458        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1459
1460        // The loaded registry should be empty
1461        assert!(loaded.is_empty());
1462    }
1463
1464    #[test]
1465    fn test_whitespace_only_file() {
1466        let temp_dir = tempfile::tempdir().unwrap();
1467        let file_path = temp_dir.path().join("whitespace_patterns.json");
1468
1469        // Create file with only whitespace
1470        fs::write(&file_path, "   \n\t  \n  ").unwrap();
1471
1472        let registry = PatternRegistry::from_file(&file_path).unwrap();
1473
1474        // Should be empty (empty file handling)
1475        assert!(registry.is_empty());
1476    }
1477
1478    #[test]
1479    fn test_save_to_default_file_path() {
1480        // Create a unique temp path to avoid conflicts
1481        let temp_dir = tempfile::tempdir().unwrap();
1482        let custom_path = temp_dir.path().join(".matrix").join("patterns.json");
1483
1484        // Manually create and save
1485        let mut registry = PatternRegistry::new();
1486        let pattern = ConversationPattern::manual(PatternType::Code, "default-file-test")
1487            .with_description("Test save_to_file with custom path");
1488        registry.add_pattern(pattern);
1489
1490        registry.save_to_file(&custom_path).unwrap();
1491
1492        // Verify file exists
1493        assert!(custom_path.exists());
1494
1495        // Load and verify
1496        let loaded = PatternRegistry::from_file(&custom_path).unwrap();
1497        assert!(loaded.patterns.iter().any(|p| p.pattern == "default-file-test"));
1498    }
1499
1500    #[test]
1501    fn test_atomic_write_rollback_safety() {
1502        let temp_dir = tempfile::tempdir().unwrap();
1503        let file_path = temp_dir.path().join("atomic_test.json");
1504
1505        // Create initial valid file
1506        let mut registry = PatternRegistry::new();
1507        registry.add_pattern(ConversationPattern::manual(PatternType::Code, "initial-pattern"));
1508        registry.save_to_file(&file_path).unwrap();
1509
1510        // Verify temp file is cleaned up
1511        let tmp_path = file_path.with_extension("json.tmp");
1512        assert!(!tmp_path.exists(), "Temp file should not exist after save");
1513
1514        // Verify the main file exists
1515        assert!(file_path.exists());
1516
1517        // Load to verify content is valid
1518        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1519        assert!(loaded.patterns.iter().any(|p| p.pattern == "initial-pattern"));
1520    }
1521
1522    #[test]
1523    fn test_long_description_and_tags() {
1524        let temp_dir = tempfile::tempdir().unwrap();
1525        let file_path = temp_dir.path().join("long_content.json");
1526
1527        // Create patterns with very long content
1528        let long_desc = "x".repeat(10000);
1529        let many_tags: Vec<String> = (0..100).map(|i| format!("tag-{}", i)).collect();
1530
1531        let mut registry = PatternRegistry::new();
1532        let mut pattern = ConversationPattern::manual(PatternType::Code, "long-pattern");
1533        pattern.description = Some(long_desc.clone());
1534        pattern.tags = many_tags.clone();
1535
1536        registry.add_pattern(pattern);
1537
1538        // Save
1539        registry.save_to_file(&file_path).unwrap();
1540
1541        // Load
1542        let loaded = PatternRegistry::from_file(&file_path).unwrap();
1543
1544        // Verify long content preserved
1545        let found = loaded.patterns.iter().find(|p| p.pattern == "long-pattern").unwrap();
1546        assert_eq!(found.description, Some(long_desc));
1547        assert_eq!(found.tags.len(), 100);
1548        for i in 0..100 {
1549            assert!(found.tags.contains(&format!("tag-{}", i)));
1550        }
1551    }
1552}
1553
1554// Helper method for tests - get pattern by pattern string
1555impl PatternRegistry {
1556    fn get_pattern_by_pattern(&self, pattern_str: &str) -> Option<&ConversationPattern> {
1557        self.patterns.iter().find(|p| p.pattern == pattern_str)
1558    }
1559}