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