Skip to main content

mockforge_vbr/
mutation_rules.rs

1//! Time-triggered data mutation rules
2//!
3//! This module provides a system for automatically mutating VBR entity data
4//! based on time triggers. It supports both aging-style rules (expiration-based)
5//! and arbitrary field mutations (time-triggered changes).
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use mockforge_vbr::mutation_rules::{MutationRule, MutationRuleManager, MutationTrigger, MutationOperation};
11//!
12//! let manager = MutationRuleManager::new();
13//!
14//! // Create a rule that increments a counter every hour
15//! let rule = MutationRule::new(
16//!     "hourly-counter".to_string(),
17//!     "User".to_string(),
18//!     MutationTrigger::Interval {
19//!         duration_seconds: 3600,
20//!     },
21//!     MutationOperation::Increment {
22//!         field: "login_count".to_string(),
23//!         amount: 1.0,
24//!     },
25//! );
26//!
27//! // Add the rule (async operation)
28//! // manager.add_rule(rule).await;
29//! ```
30
31use crate::{Error, Result};
32use chrono::{DateTime, Duration, Utc};
33use mockforge_core::time_travel_now;
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36use std::collections::HashMap;
37use std::sync::Arc;
38use tokio::sync::RwLock;
39use tracing::{debug, info, warn};
40
41/// Trigger condition for a mutation rule
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "type", rename_all = "lowercase")]
44pub enum MutationTrigger {
45    /// Trigger after a duration has elapsed
46    Interval {
47        /// Duration in seconds
48        duration_seconds: u64,
49    },
50    /// Trigger at a specific time (cron-like, but simpler)
51    AtTime {
52        /// Hour (0-23)
53        hour: u8,
54        /// Minute (0-59)
55        minute: u8,
56    },
57    /// Trigger when a field value reaches a threshold
58    FieldThreshold {
59        /// Field to check
60        field: String,
61        /// Threshold value
62        threshold: Value,
63        /// Comparison operator
64        operator: ComparisonOperator,
65    },
66}
67
68/// Comparison operator for field threshold triggers
69#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "lowercase")]
71pub enum ComparisonOperator {
72    /// Greater than
73    Gt,
74    /// Less than
75    Lt,
76    /// Equal to
77    Eq,
78    /// Greater than or equal
79    Gte,
80    /// Less than or equal
81    Lte,
82}
83
84/// Mutation operation to perform
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "type", rename_all = "lowercase")]
87pub enum MutationOperation {
88    /// Set a field to a specific value
89    Set {
90        /// Field name
91        field: String,
92        /// Value to set
93        value: Value,
94    },
95    /// Increment a numeric field
96    Increment {
97        /// Field name
98        field: String,
99        /// Amount to increment by
100        amount: f64,
101    },
102    /// Decrement a numeric field
103    Decrement {
104        /// Field name
105        field: String,
106        /// Amount to decrement by
107        amount: f64,
108    },
109    /// Transform a field using a template or expression
110    Transform {
111        /// Field name
112        field: String,
113        /// Transformation expression (e.g., "{{field}} * 2")
114        expression: String,
115    },
116    /// Update status field
117    UpdateStatus {
118        /// New status value
119        status: String,
120    },
121}
122
123/// A mutation rule definition
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct MutationRule {
126    /// Unique identifier for this rule
127    pub id: String,
128    /// Entity name to apply mutation to
129    pub entity_name: String,
130    /// Trigger condition
131    pub trigger: MutationTrigger,
132    /// Mutation operation
133    pub operation: MutationOperation,
134    /// Whether this rule is enabled
135    #[serde(default = "default_true")]
136    pub enabled: bool,
137    /// Optional description
138    #[serde(default)]
139    pub description: Option<String>,
140    /// Optional condition (JSONPath expression) that must be true
141    #[serde(default)]
142    pub condition: Option<String>,
143    /// Last execution time
144    #[serde(default)]
145    pub last_execution: Option<DateTime<Utc>>,
146    /// Next scheduled execution time
147    #[serde(default)]
148    pub next_execution: Option<DateTime<Utc>>,
149    /// Number of times this rule has executed
150    #[serde(default)]
151    pub execution_count: usize,
152}
153
154fn default_true() -> bool {
155    true
156}
157
158impl MutationRule {
159    /// Create a new mutation rule
160    pub fn new(
161        id: String,
162        entity_name: String,
163        trigger: MutationTrigger,
164        operation: MutationOperation,
165    ) -> Self {
166        Self {
167            id,
168            entity_name,
169            trigger,
170            operation,
171            enabled: true,
172            description: None,
173            condition: None,
174            last_execution: None,
175            next_execution: None,
176            execution_count: 0,
177        }
178    }
179
180    /// Calculate the next execution time based on the trigger
181    pub fn calculate_next_execution(&self, from: DateTime<Utc>) -> Option<DateTime<Utc>> {
182        if !self.enabled {
183            return None;
184        }
185
186        match &self.trigger {
187            MutationTrigger::Interval { duration_seconds } => {
188                Some(from + Duration::seconds(*duration_seconds as i64))
189            }
190            MutationTrigger::AtTime { hour, minute } => {
191                // Calculate next occurrence of this time
192                let mut next =
193                    from.date_naive().and_hms_opt(*hour as u32, *minute as u32, 0)?.and_utc();
194
195                // If the time has already passed today, move to tomorrow
196                if next <= from {
197                    next += Duration::days(1);
198                }
199
200                Some(next)
201            }
202            MutationTrigger::FieldThreshold { .. } => {
203                // Field threshold triggers are evaluated on-demand, not scheduled
204                None
205            }
206        }
207    }
208}
209
210/// Manager for mutation rules
211pub struct MutationRuleManager {
212    /// Registered mutation rules
213    rules: Arc<RwLock<HashMap<String, MutationRule>>>,
214}
215
216impl MutationRuleManager {
217    /// Create a new mutation rule manager
218    pub fn new() -> Self {
219        Self {
220            rules: Arc::new(RwLock::new(HashMap::new())),
221        }
222    }
223
224    /// Add a mutation rule
225    pub async fn add_rule(&self, mut rule: MutationRule) -> Result<()> {
226        // Calculate next execution time
227        let now = time_travel_now();
228        rule.next_execution = rule.calculate_next_execution(now);
229
230        let rule_id = rule.id.clone();
231
232        let mut rules = self.rules.write().await;
233        rules.insert(rule_id.clone(), rule);
234
235        info!("Added mutation rule '{}' for entity '{}'", rule_id, rules[&rule_id].entity_name);
236        Ok(())
237    }
238
239    /// Remove a mutation rule
240    pub async fn remove_rule(&self, rule_id: &str) -> bool {
241        let mut rules = self.rules.write().await;
242        let removed = rules.remove(rule_id).is_some();
243
244        if removed {
245            info!("Removed mutation rule '{}'", rule_id);
246        }
247
248        removed
249    }
250
251    /// Get a mutation rule by ID
252    pub async fn get_rule(&self, rule_id: &str) -> Option<MutationRule> {
253        let rules = self.rules.read().await;
254        rules.get(rule_id).cloned()
255    }
256
257    /// List all mutation rules
258    pub async fn list_rules(&self) -> Vec<MutationRule> {
259        let rules = self.rules.read().await;
260        rules.values().cloned().collect()
261    }
262
263    /// List rules for a specific entity
264    pub async fn list_rules_for_entity(&self, entity_name: &str) -> Vec<MutationRule> {
265        let rules = self.rules.read().await;
266        rules.values().filter(|rule| rule.entity_name == entity_name).cloned().collect()
267    }
268
269    /// Enable or disable a mutation rule
270    pub async fn set_rule_enabled(&self, rule_id: &str, enabled: bool) -> Result<()> {
271        let mut rules = self.rules.write().await;
272
273        if let Some(rule) = rules.get_mut(rule_id) {
274            rule.enabled = enabled;
275
276            // Recalculate next execution if enabling
277            if enabled {
278                let now = time_travel_now();
279                rule.next_execution = rule.calculate_next_execution(now);
280            } else {
281                rule.next_execution = None;
282            }
283
284            info!("Mutation rule '{}' {}", rule_id, if enabled { "enabled" } else { "disabled" });
285            Ok(())
286        } else {
287            Err(Error::generic(format!("Mutation rule '{}' not found", rule_id)))
288        }
289    }
290
291    /// Check for rules that should execute now and execute them
292    ///
293    /// This should be called periodically or when time advances
294    /// to check if any rules are due for execution.
295    pub async fn check_and_execute(
296        &self,
297        database: &dyn crate::database::VirtualDatabase,
298        registry: &crate::entities::EntityRegistry,
299    ) -> Result<usize> {
300        let now = time_travel_now();
301        let mut executed = 0;
302
303        // Get rules that need to execute
304        let mut rules_to_execute = Vec::new();
305
306        {
307            let rules = self.rules.read().await;
308            for rule in rules.values() {
309                if !rule.enabled {
310                    continue;
311                }
312
313                if let Some(next) = rule.next_execution {
314                    if now >= next {
315                        rules_to_execute.push(rule.id.clone());
316                    }
317                }
318            }
319        }
320
321        // Execute rules
322        for rule_id in rules_to_execute {
323            if let Err(e) = self.execute_rule(&rule_id, database, registry).await {
324                warn!("Error executing mutation rule '{}': {}", rule_id, e);
325            } else {
326                executed += 1;
327            }
328        }
329
330        Ok(executed)
331    }
332
333    /// Evaluate a transformation expression
334    ///
335    /// Supports expressions like:
336    /// - "{{field}} * 2" - multiply field by 2
337    /// - "{{field1}} + {{field2}}" - add two fields
338    /// - "{{field}}.toUpperCase()" - string operations
339    /// - "{{field}} + 10" - add constant
340    /// - Simple template strings with variable substitution
341    fn evaluate_transformation_expression(
342        expression: &str,
343        record: &HashMap<String, Value>,
344    ) -> Result<Value> {
345        use regex::Regex;
346
347        // Convert record to Value for easier manipulation
348        let record_value: Value =
349            Value::Object(record.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
350
351        // Substitute variables in expression (e.g., "{{field}}" -> actual value)
352        let re = Regex::new(r"\{\{([^}]+)\}\}")
353            .map_err(|e| Error::generic(format!("Failed to compile regex: {}", e)))?;
354
355        let substituted = re.replace_all(expression, |caps: &regex::Captures| {
356            let var_name = caps.get(1).unwrap().as_str().trim();
357            // Try to get value from record
358            if let Some(value) = record.get(var_name) {
359                // Convert to string representation for substitution
360                if let Some(s) = value.as_str() {
361                    s.to_string()
362                } else if let Some(n) = value.as_f64() {
363                    n.to_string()
364                } else if let Some(b) = value.as_bool() {
365                    b.to_string()
366                } else {
367                    value.to_string()
368                }
369            } else {
370                // If not found, try JSONPath expression
371                if var_name.starts_with('$') {
372                    // Use JSONPath to extract value
373                    if let Ok(selector) = jsonpath::Selector::new(var_name) {
374                        let results: Vec<_> = selector.find(&record_value).collect();
375                        if let Some(first) = results.first() {
376                            if let Some(s) = first.as_str() {
377                                return s.to_string();
378                            } else if let Some(n) = first.as_f64() {
379                                return n.to_string();
380                            } else if let Some(b) = first.as_bool() {
381                                return b.to_string();
382                            }
383                        }
384                    }
385                }
386                format!("{{{{{}}}}}", var_name) // Keep original if not found
387            }
388        });
389
390        // Try to evaluate as a mathematical expression
391        let substituted_str = substituted.to_string();
392
393        // Check for mathematical operations
394        if substituted_str.contains('+')
395            || substituted_str.contains('-')
396            || substituted_str.contains('*')
397            || substituted_str.contains('/')
398        {
399            // Try to parse and evaluate as math expression
400            if let Ok(result) = Self::evaluate_math_expression(&substituted_str) {
401                return Ok(serde_json::json!(result));
402            }
403        }
404
405        // Check for string operations
406        if substituted_str.contains(".toUpperCase()") {
407            let base = substituted_str.replace(".toUpperCase()", "");
408            return Ok(Value::String(base.to_uppercase()));
409        }
410        if substituted_str.contains(".toLowerCase()") {
411            let base = substituted_str.replace(".toLowerCase()", "");
412            return Ok(Value::String(base.to_lowercase()));
413        }
414        if substituted_str.contains(".trim()") {
415            let base = substituted_str.replace(".trim()", "");
416            return Ok(Value::String(base.trim().to_string()));
417        }
418
419        // If no operations detected, return as string
420        Ok(Value::String(substituted_str))
421    }
422
423    /// Evaluate a simple mathematical expression
424    ///
425    /// Supports basic operations: +, -, *, /
426    /// Example: "10 + 5 * 2" -> 20
427    fn evaluate_math_expression(expr: &str) -> Result<f64> {
428        // Simple expression evaluator (handles basic arithmetic)
429        // For more complex expressions, consider using a proper expression parser
430
431        // Remove whitespace
432        let expr = expr.replace(' ', "");
433
434        // Try to parse as a simple expression
435        // This is a simplified evaluator - for production, use a proper math parser
436        let mut result = 0.0;
437        let mut current_num = String::new();
438        let mut last_op = '+';
439
440        for ch in expr.chars() {
441            match ch {
442                '+' | '-' | '*' | '/' => {
443                    if !current_num.is_empty() {
444                        let num: f64 = current_num.parse().map_err(|_| {
445                            Error::generic(format!("Invalid number: {}", current_num))
446                        })?;
447
448                        match last_op {
449                            '+' => result += num,
450                            '-' => result -= num,
451                            '*' => result *= num,
452                            '/' => {
453                                if num == 0.0 {
454                                    return Err(Error::generic("Division by zero".to_string()));
455                                }
456                                result /= num;
457                            }
458                            _ => {}
459                        }
460
461                        current_num.clear();
462                    }
463                    last_op = ch;
464                }
465                '0'..='9' | '.' => {
466                    current_num.push(ch);
467                }
468                _ => {
469                    return Err(Error::generic(format!("Invalid character in expression: {}", ch)));
470                }
471            }
472        }
473
474        // Handle last number
475        if !current_num.is_empty() {
476            let num: f64 = current_num
477                .parse()
478                .map_err(|_| Error::generic(format!("Invalid number: {}", current_num)))?;
479
480            match last_op {
481                '+' => result += num,
482                '-' => result -= num,
483                '*' => result *= num,
484                '/' => {
485                    if num == 0.0 {
486                        return Err(Error::generic("Division by zero".to_string()));
487                    }
488                    result /= num;
489                }
490                _ => result = num, // First number
491            }
492        }
493
494        Ok(result)
495    }
496
497    /// Evaluate a JSONPath condition against a record
498    ///
499    /// The condition can be:
500    /// - A simple JSONPath expression that checks for existence (e.g., "$.status")
501    /// - A JSONPath expression with comparison (e.g., "$.status == 'active'")
502    /// - A boolean JSONPath expression (e.g., "$.enabled")
503    ///
504    /// Returns true if the condition is met, false otherwise.
505    fn evaluate_condition(condition: &str, record: &Value) -> Result<bool> {
506        // Simple JSONPath evaluation
507        // For basic existence checks (e.g., "$.field"), check if path exists and is truthy
508        // For comparison expressions (e.g., "$.field == 'value'"), parse and evaluate
509
510        // Try to parse as JSONPath selector
511        if let Ok(selector) = jsonpath::Selector::new(condition) {
512            // If condition is just a path (no comparison), check if it exists and is truthy
513            let results: Vec<_> = selector.find(record).collect();
514            if !results.is_empty() {
515                // Check if any result is truthy
516                for result in results {
517                    match result {
518                        Value::Bool(b) => {
519                            if *b {
520                                return Ok(true);
521                            }
522                        }
523                        Value::Null => continue,
524                        Value::String(s) => {
525                            if !s.is_empty() {
526                                return Ok(true);
527                            }
528                        }
529                        Value::Number(n) => {
530                            if n.as_f64().map(|f| f != 0.0).unwrap_or(false) {
531                                return Ok(true);
532                            }
533                        }
534                        _ => return Ok(true), // Other types (objects, arrays) are truthy
535                    }
536                }
537            }
538            return Ok(false);
539        }
540
541        // If JSONPath parsing fails, try to parse as a comparison expression
542        // Simple pattern: "$.field == 'value'" or "$.field > 10"
543        if condition.contains("==") {
544            let parts: Vec<&str> = condition.split("==").map(|s| s.trim()).collect();
545            if parts.len() == 2 {
546                let path = parts[0].trim();
547                let expected = parts[1].trim().trim_matches('\'').trim_matches('"');
548
549                if let Ok(selector) = jsonpath::Selector::new(path) {
550                    let results: Vec<_> = selector.find(record).collect();
551                    for result in results {
552                        match result {
553                            Value::String(s) if s == expected => return Ok(true),
554                            Value::Number(n) => {
555                                if let Ok(expected_num) = expected.parse::<f64>() {
556                                    if n.as_f64().map(|f| f == expected_num).unwrap_or(false) {
557                                        return Ok(true);
558                                    }
559                                }
560                            }
561                            _ => {}
562                        }
563                    }
564                }
565            }
566        } else if condition.contains(">") {
567            let parts: Vec<&str> = condition.split(">").map(|s| s.trim()).collect();
568            if parts.len() == 2 {
569                let path = parts[0].trim();
570                if let Ok(expected_num) = parts[1].trim().parse::<f64>() {
571                    if let Ok(selector) = jsonpath::Selector::new(path) {
572                        let results: Vec<_> = selector.find(record).collect();
573                        for result in results {
574                            if let Value::Number(n) = result {
575                                if n.as_f64().map(|f| f > expected_num).unwrap_or(false) {
576                                    return Ok(true);
577                                }
578                            }
579                        }
580                    }
581                }
582            }
583        } else if condition.contains("<") {
584            let parts: Vec<&str> = condition.split("<").map(|s| s.trim()).collect();
585            if parts.len() == 2 {
586                let path = parts[0].trim();
587                if let Ok(expected_num) = parts[1].trim().parse::<f64>() {
588                    if let Ok(selector) = jsonpath::Selector::new(path) {
589                        let results: Vec<_> = selector.find(record).collect();
590                        for result in results {
591                            if let Value::Number(n) = result {
592                                if n.as_f64().map(|f| f < expected_num).unwrap_or(false) {
593                                    return Ok(true);
594                                }
595                            }
596                        }
597                    }
598                }
599            }
600        }
601
602        // If we can't parse the condition, log a warning and return false
603        warn!("Could not evaluate condition '{}', treating as false", condition);
604        Ok(false)
605    }
606
607    /// Execute a specific mutation rule
608    async fn execute_rule(
609        &self,
610        rule_id: &str,
611        database: &dyn crate::database::VirtualDatabase,
612        registry: &crate::entities::EntityRegistry,
613    ) -> Result<()> {
614        let now = time_travel_now();
615
616        // Get rule
617        let rule = {
618            let rules = self.rules.read().await;
619            rules
620                .get(rule_id)
621                .ok_or_else(|| Error::generic(format!("Mutation rule '{}' not found", rule_id)))?
622                .clone()
623        };
624
625        // Get entity info
626        let entity = registry
627            .get(&rule.entity_name)
628            .ok_or_else(|| Error::generic(format!("Entity '{}' not found", rule.entity_name)))?;
629
630        let table_name = entity.table_name();
631
632        // Query all records for this entity
633        let query = format!("SELECT * FROM {}", table_name);
634        let records = database.query(&query, &[]).await?;
635
636        // Apply mutation to each record
637        let pk_field = entity.schema.primary_key.first().map(|s| s.as_str()).unwrap_or("id");
638
639        for record in records {
640            // Check condition if specified
641            if let Some(ref condition) = rule.condition {
642                // Convert HashMap to Value for JSONPath evaluation
643                let record_value =
644                    Value::Object(record.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
645
646                // Evaluate JSONPath condition
647                // Condition should be a JSONPath expression that evaluates to a truthy value
648                // Examples: "$.status == 'active'", "$.age > 18", "$.enabled"
649                if !MutationRuleManager::evaluate_condition(condition, &record_value)? {
650                    debug!("Condition '{}' not met for record, skipping", condition);
651                    continue;
652                }
653            }
654
655            // Get primary key value
656            let pk_value = record
657                .get(pk_field)
658                .ok_or_else(|| Error::generic(format!("Primary key '{}' not found", pk_field)))?;
659
660            // Apply mutation operation
661            match &rule.operation {
662                MutationOperation::Set { field, value } => {
663                    let update_query =
664                        format!("UPDATE {} SET {} = ? WHERE {} = ?", table_name, field, pk_field);
665                    database.execute(&update_query, &[value.clone(), pk_value.clone()]).await?;
666                }
667                MutationOperation::Increment { field, amount } => {
668                    // Get current value
669                    if let Some(current) = record.get(field) {
670                        let new_value = if let Some(num) = current.as_f64() {
671                            Value::Number(
672                                serde_json::Number::from_f64(num + amount)
673                                    .unwrap_or_else(|| serde_json::Number::from(0)),
674                            )
675                        } else if let Some(num) = current.as_i64() {
676                            Value::Number(serde_json::Number::from(num + *amount as i64))
677                        } else {
678                            continue; // Skip non-numeric fields
679                        };
680
681                        let update_query = format!(
682                            "UPDATE {} SET {} = ? WHERE {} = ?",
683                            table_name, field, pk_field
684                        );
685                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
686                    }
687                }
688                MutationOperation::Decrement { field, amount } => {
689                    // Get current value
690                    if let Some(current) = record.get(field) {
691                        let new_value = if let Some(num) = current.as_f64() {
692                            Value::Number(
693                                serde_json::Number::from_f64(num - amount)
694                                    .unwrap_or_else(|| serde_json::Number::from(0)),
695                            )
696                        } else if let Some(num) = current.as_i64() {
697                            Value::Number(serde_json::Number::from(num - *amount as i64))
698                        } else {
699                            continue; // Skip non-numeric fields
700                        };
701
702                        let update_query = format!(
703                            "UPDATE {} SET {} = ? WHERE {} = ?",
704                            table_name, field, pk_field
705                        );
706                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
707                    }
708                }
709                MutationOperation::Transform { field, expression } => {
710                    // Evaluate transformation expression
711                    let transformed_value =
712                        Self::evaluate_transformation_expression(expression, &record)?;
713
714                    let update_query =
715                        format!("UPDATE {} SET {} = ? WHERE {} = ?", table_name, field, pk_field);
716                    database.execute(&update_query, &[transformed_value, pk_value.clone()]).await?;
717                }
718                MutationOperation::UpdateStatus { status } => {
719                    let update_query =
720                        format!("UPDATE {} SET status = ? WHERE {} = ?", table_name, pk_field);
721                    database
722                        .execute(&update_query, &[Value::String(status.clone()), pk_value.clone()])
723                        .await?;
724                }
725            }
726        }
727
728        // Update rule state
729        {
730            let mut rules = self.rules.write().await;
731            if let Some(rule) = rules.get_mut(rule_id) {
732                rule.last_execution = Some(now);
733                rule.execution_count += 1;
734
735                // Calculate next execution
736                rule.next_execution = rule.calculate_next_execution(now);
737            }
738        }
739
740        info!("Executed mutation rule '{}' on entity '{}'", rule_id, rule.entity_name);
741        Ok(())
742    }
743}
744
745impl Default for MutationRuleManager {
746    fn default() -> Self {
747        Self::new()
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    // MutationTrigger tests
756    #[test]
757    fn test_mutation_trigger_interval_serialize() {
758        let trigger = MutationTrigger::Interval {
759            duration_seconds: 3600,
760        };
761        let json = serde_json::to_string(&trigger).unwrap();
762        assert!(json.contains("interval"));
763        assert!(json.contains("3600"));
764    }
765
766    #[test]
767    fn test_mutation_trigger_at_time_serialize() {
768        let trigger = MutationTrigger::AtTime {
769            hour: 9,
770            minute: 30,
771        };
772        let json = serde_json::to_string(&trigger).unwrap();
773        assert!(json.contains("attime"));
774        assert!(json.contains("\"hour\":9"));
775    }
776
777    #[test]
778    fn test_mutation_trigger_field_threshold_serialize() {
779        let trigger = MutationTrigger::FieldThreshold {
780            field: "age".to_string(),
781            threshold: serde_json::json!(100),
782            operator: ComparisonOperator::Gt,
783        };
784        let json = serde_json::to_string(&trigger).unwrap();
785        assert!(json.contains("fieldthreshold"));
786        assert!(json.contains("age"));
787    }
788
789    #[test]
790    fn test_mutation_trigger_clone() {
791        let trigger = MutationTrigger::Interval {
792            duration_seconds: 60,
793        };
794        let cloned = trigger.clone();
795        match cloned {
796            MutationTrigger::Interval { duration_seconds } => {
797                assert_eq!(duration_seconds, 60);
798            }
799            _ => panic!("Expected Interval variant"),
800        }
801    }
802
803    #[test]
804    fn test_mutation_trigger_debug() {
805        let trigger = MutationTrigger::Interval {
806            duration_seconds: 120,
807        };
808        let debug = format!("{:?}", trigger);
809        assert!(debug.contains("Interval"));
810    }
811
812    // MutationOperation tests
813    #[test]
814    fn test_mutation_operation_set() {
815        let op = MutationOperation::Set {
816            field: "status".to_string(),
817            value: serde_json::json!("active"),
818        };
819        let json = serde_json::to_string(&op).unwrap();
820        assert!(json.contains("set"));
821        assert!(json.contains("status"));
822    }
823
824    #[test]
825    fn test_mutation_operation_increment() {
826        let op = MutationOperation::Increment {
827            field: "count".to_string(),
828            amount: 5.0,
829        };
830        let json = serde_json::to_string(&op).unwrap();
831        assert!(json.contains("increment"));
832        assert!(json.contains("count"));
833    }
834
835    #[test]
836    fn test_mutation_operation_decrement() {
837        let op = MutationOperation::Decrement {
838            field: "balance".to_string(),
839            amount: 10.5,
840        };
841        let json = serde_json::to_string(&op).unwrap();
842        assert!(json.contains("decrement"));
843        assert!(json.contains("balance"));
844    }
845
846    #[test]
847    fn test_mutation_operation_transform() {
848        let op = MutationOperation::Transform {
849            field: "value".to_string(),
850            expression: "{{value}} * 2".to_string(),
851        };
852        let json = serde_json::to_string(&op).unwrap();
853        assert!(json.contains("transform"));
854    }
855
856    #[test]
857    fn test_mutation_operation_update_status() {
858        let op = MutationOperation::UpdateStatus {
859            status: "completed".to_string(),
860        };
861        let json = serde_json::to_string(&op).unwrap();
862        assert!(json.contains("updatestatus"));
863        assert!(json.contains("completed"));
864    }
865
866    #[test]
867    fn test_mutation_operation_clone() {
868        let op = MutationOperation::Set {
869            field: "test".to_string(),
870            value: serde_json::json!(42),
871        };
872        let cloned = op.clone();
873        match cloned {
874            MutationOperation::Set { field, value } => {
875                assert_eq!(field, "test");
876                assert_eq!(value, serde_json::json!(42));
877            }
878            _ => panic!("Expected Set variant"),
879        }
880    }
881
882    // MutationRule tests
883    #[test]
884    fn test_mutation_rule_creation() {
885        let rule = MutationRule::new(
886            "test-1".to_string(),
887            "User".to_string(),
888            MutationTrigger::Interval {
889                duration_seconds: 3600,
890            },
891            MutationOperation::Increment {
892                field: "count".to_string(),
893                amount: 1.0,
894            },
895        );
896
897        assert_eq!(rule.id, "test-1");
898        assert_eq!(rule.entity_name, "User");
899        assert!(rule.enabled);
900        assert!(rule.description.is_none());
901        assert!(rule.condition.is_none());
902        assert!(rule.last_execution.is_none());
903        assert_eq!(rule.execution_count, 0);
904    }
905
906    #[test]
907    fn test_mutation_rule_defaults() {
908        let rule = MutationRule::new(
909            "rule-1".to_string(),
910            "Order".to_string(),
911            MutationTrigger::Interval {
912                duration_seconds: 60,
913            },
914            MutationOperation::Set {
915                field: "status".to_string(),
916                value: serde_json::json!("processed"),
917            },
918        );
919
920        // Default values
921        assert!(rule.enabled);
922        assert!(rule.description.is_none());
923        assert!(rule.condition.is_none());
924        assert!(rule.last_execution.is_none());
925        assert_eq!(rule.execution_count, 0);
926    }
927
928    #[test]
929    fn test_mutation_rule_clone() {
930        let rule = MutationRule::new(
931            "clone-test".to_string(),
932            "Entity".to_string(),
933            MutationTrigger::Interval {
934                duration_seconds: 100,
935            },
936            MutationOperation::Increment {
937                field: "counter".to_string(),
938                amount: 1.0,
939            },
940        );
941
942        let cloned = rule.clone();
943        assert_eq!(rule.id, cloned.id);
944        assert_eq!(rule.entity_name, cloned.entity_name);
945        assert_eq!(rule.enabled, cloned.enabled);
946    }
947
948    #[test]
949    fn test_mutation_rule_debug() {
950        let rule = MutationRule::new(
951            "debug-rule".to_string(),
952            "Test".to_string(),
953            MutationTrigger::Interval {
954                duration_seconds: 10,
955            },
956            MutationOperation::Set {
957                field: "f".to_string(),
958                value: serde_json::json!("v"),
959            },
960        );
961
962        let debug = format!("{:?}", rule);
963        assert!(debug.contains("MutationRule"));
964        assert!(debug.contains("debug-rule"));
965    }
966
967    #[test]
968    fn test_mutation_trigger_interval() {
969        let rule = MutationRule::new(
970            "test-1".to_string(),
971            "User".to_string(),
972            MutationTrigger::Interval {
973                duration_seconds: 3600,
974            },
975            MutationOperation::Set {
976                field: "status".to_string(),
977                value: serde_json::json!("active"),
978            },
979        );
980
981        let now = Utc::now();
982        let next = rule.calculate_next_execution(now).unwrap();
983        let duration = next - now;
984
985        // Should be approximately 1 hour
986        assert!(duration.num_seconds() >= 3599 && duration.num_seconds() <= 3601);
987    }
988
989    #[test]
990    fn test_mutation_rule_calculate_next_execution_disabled() {
991        let mut rule = MutationRule::new(
992            "disabled-rule".to_string(),
993            "Entity".to_string(),
994            MutationTrigger::Interval {
995                duration_seconds: 60,
996            },
997            MutationOperation::Set {
998                field: "f".to_string(),
999                value: serde_json::json!("v"),
1000            },
1001        );
1002        rule.enabled = false;
1003
1004        let now = Utc::now();
1005        assert!(rule.calculate_next_execution(now).is_none());
1006    }
1007
1008    #[test]
1009    fn test_mutation_rule_calculate_next_execution_field_threshold() {
1010        let rule = MutationRule::new(
1011            "threshold-rule".to_string(),
1012            "Entity".to_string(),
1013            MutationTrigger::FieldThreshold {
1014                field: "value".to_string(),
1015                threshold: serde_json::json!(100),
1016                operator: ComparisonOperator::Gt,
1017            },
1018            MutationOperation::Set {
1019                field: "f".to_string(),
1020                value: serde_json::json!("v"),
1021            },
1022        );
1023
1024        // Field threshold triggers don't have scheduled execution
1025        let now = Utc::now();
1026        assert!(rule.calculate_next_execution(now).is_none());
1027    }
1028
1029    // MutationRuleManager tests
1030    #[tokio::test]
1031    async fn test_mutation_rule_manager() {
1032        let manager = MutationRuleManager::new();
1033
1034        let rule = MutationRule::new(
1035            "test-1".to_string(),
1036            "User".to_string(),
1037            MutationTrigger::Interval {
1038                duration_seconds: 3600,
1039            },
1040            MutationOperation::Increment {
1041                field: "count".to_string(),
1042                amount: 1.0,
1043            },
1044        );
1045
1046        manager.add_rule(rule).await.unwrap();
1047
1048        let rules = manager.list_rules().await;
1049        assert_eq!(rules.len(), 1);
1050        assert_eq!(rules[0].id, "test-1");
1051    }
1052
1053    #[tokio::test]
1054    async fn test_mutation_rule_manager_new() {
1055        let manager = MutationRuleManager::new();
1056        let rules = manager.list_rules().await;
1057        assert!(rules.is_empty());
1058    }
1059
1060    #[tokio::test]
1061    async fn test_mutation_rule_manager_default() {
1062        let manager = MutationRuleManager::default();
1063        let rules = manager.list_rules().await;
1064        assert!(rules.is_empty());
1065    }
1066
1067    #[tokio::test]
1068    async fn test_mutation_rule_manager_add_and_get_rule() {
1069        let manager = MutationRuleManager::new();
1070
1071        let rule = MutationRule::new(
1072            "get-test".to_string(),
1073            "Order".to_string(),
1074            MutationTrigger::Interval {
1075                duration_seconds: 60,
1076            },
1077            MutationOperation::Set {
1078                field: "status".to_string(),
1079                value: serde_json::json!("done"),
1080            },
1081        );
1082
1083        manager.add_rule(rule).await.unwrap();
1084
1085        let retrieved = manager.get_rule("get-test").await;
1086        assert!(retrieved.is_some());
1087        assert_eq!(retrieved.unwrap().id, "get-test");
1088    }
1089
1090    #[tokio::test]
1091    async fn test_mutation_rule_manager_get_nonexistent() {
1092        let manager = MutationRuleManager::new();
1093        let retrieved = manager.get_rule("nonexistent").await;
1094        assert!(retrieved.is_none());
1095    }
1096
1097    #[tokio::test]
1098    async fn test_mutation_rule_manager_remove_rule() {
1099        let manager = MutationRuleManager::new();
1100
1101        let rule = MutationRule::new(
1102            "remove-test".to_string(),
1103            "Entity".to_string(),
1104            MutationTrigger::Interval {
1105                duration_seconds: 60,
1106            },
1107            MutationOperation::Set {
1108                field: "f".to_string(),
1109                value: serde_json::json!("v"),
1110            },
1111        );
1112
1113        manager.add_rule(rule).await.unwrap();
1114        assert!(manager.get_rule("remove-test").await.is_some());
1115
1116        let removed = manager.remove_rule("remove-test").await;
1117        assert!(removed);
1118        assert!(manager.get_rule("remove-test").await.is_none());
1119    }
1120
1121    #[tokio::test]
1122    async fn test_mutation_rule_manager_remove_nonexistent() {
1123        let manager = MutationRuleManager::new();
1124        let removed = manager.remove_rule("nonexistent").await;
1125        assert!(!removed);
1126    }
1127
1128    #[tokio::test]
1129    async fn test_mutation_rule_manager_list_rules() {
1130        let manager = MutationRuleManager::new();
1131
1132        // Add multiple rules
1133        for i in 1..=3 {
1134            let rule = MutationRule::new(
1135                format!("rule-{}", i),
1136                "Entity".to_string(),
1137                MutationTrigger::Interval {
1138                    duration_seconds: 60,
1139                },
1140                MutationOperation::Set {
1141                    field: "f".to_string(),
1142                    value: serde_json::json!("v"),
1143                },
1144            );
1145            manager.add_rule(rule).await.unwrap();
1146        }
1147
1148        let rules = manager.list_rules().await;
1149        assert_eq!(rules.len(), 3);
1150    }
1151
1152    #[tokio::test]
1153    async fn test_mutation_rule_manager_list_rules_for_entity() {
1154        let manager = MutationRuleManager::new();
1155
1156        // Add rules for different entities
1157        let rule1 = MutationRule::new(
1158            "user-rule".to_string(),
1159            "User".to_string(),
1160            MutationTrigger::Interval {
1161                duration_seconds: 60,
1162            },
1163            MutationOperation::Set {
1164                field: "f".to_string(),
1165                value: serde_json::json!("v"),
1166            },
1167        );
1168
1169        let rule2 = MutationRule::new(
1170            "order-rule".to_string(),
1171            "Order".to_string(),
1172            MutationTrigger::Interval {
1173                duration_seconds: 60,
1174            },
1175            MutationOperation::Set {
1176                field: "f".to_string(),
1177                value: serde_json::json!("v"),
1178            },
1179        );
1180
1181        let rule3 = MutationRule::new(
1182            "user-rule-2".to_string(),
1183            "User".to_string(),
1184            MutationTrigger::Interval {
1185                duration_seconds: 120,
1186            },
1187            MutationOperation::Increment {
1188                field: "count".to_string(),
1189                amount: 1.0,
1190            },
1191        );
1192
1193        manager.add_rule(rule1).await.unwrap();
1194        manager.add_rule(rule2).await.unwrap();
1195        manager.add_rule(rule3).await.unwrap();
1196
1197        let user_rules = manager.list_rules_for_entity("User").await;
1198        assert_eq!(user_rules.len(), 2);
1199
1200        let order_rules = manager.list_rules_for_entity("Order").await;
1201        assert_eq!(order_rules.len(), 1);
1202
1203        let product_rules = manager.list_rules_for_entity("Product").await;
1204        assert!(product_rules.is_empty());
1205    }
1206
1207    #[tokio::test]
1208    async fn test_mutation_rule_manager_set_rule_enabled() {
1209        let manager = MutationRuleManager::new();
1210
1211        let rule = MutationRule::new(
1212            "enable-test".to_string(),
1213            "Entity".to_string(),
1214            MutationTrigger::Interval {
1215                duration_seconds: 60,
1216            },
1217            MutationOperation::Set {
1218                field: "f".to_string(),
1219                value: serde_json::json!("v"),
1220            },
1221        );
1222
1223        manager.add_rule(rule).await.unwrap();
1224
1225        // Disable the rule
1226        manager.set_rule_enabled("enable-test", false).await.unwrap();
1227        let disabled_rule = manager.get_rule("enable-test").await.unwrap();
1228        assert!(!disabled_rule.enabled);
1229        assert!(disabled_rule.next_execution.is_none());
1230
1231        // Re-enable the rule
1232        manager.set_rule_enabled("enable-test", true).await.unwrap();
1233        let enabled_rule = manager.get_rule("enable-test").await.unwrap();
1234        assert!(enabled_rule.enabled);
1235        assert!(enabled_rule.next_execution.is_some());
1236    }
1237
1238    #[tokio::test]
1239    async fn test_mutation_rule_manager_set_rule_enabled_nonexistent() {
1240        let manager = MutationRuleManager::new();
1241        let result = manager.set_rule_enabled("nonexistent", true).await;
1242        assert!(result.is_err());
1243    }
1244
1245    // ComparisonOperator tests
1246    #[test]
1247    fn test_comparison_operator_variants() {
1248        let operators = vec![
1249            ComparisonOperator::Gt,
1250            ComparisonOperator::Lt,
1251            ComparisonOperator::Eq,
1252            ComparisonOperator::Gte,
1253            ComparisonOperator::Lte,
1254        ];
1255
1256        for op in operators {
1257            let json = serde_json::to_string(&op).unwrap();
1258            assert!(!json.is_empty());
1259        }
1260    }
1261
1262    #[test]
1263    fn test_comparison_operator_clone() {
1264        let op = ComparisonOperator::Gt;
1265        let cloned = op;
1266        assert!(matches!(cloned, ComparisonOperator::Gt));
1267    }
1268
1269    #[test]
1270    fn test_comparison_operator_debug() {
1271        let op = ComparisonOperator::Lt;
1272        let debug = format!("{:?}", op);
1273        assert!(debug.contains("Lt"));
1274    }
1275
1276    #[test]
1277    fn test_comparison_operator_eq() {
1278        let op1 = ComparisonOperator::Gt;
1279        let op2 = ComparisonOperator::Gt;
1280        let op3 = ComparisonOperator::Lt;
1281        assert_eq!(op1, op2);
1282        assert_ne!(op1, op3);
1283    }
1284
1285    #[test]
1286    fn test_comparison_operator_serialize() {
1287        assert_eq!(serde_json::to_string(&ComparisonOperator::Gt).unwrap(), "\"gt\"");
1288        assert_eq!(serde_json::to_string(&ComparisonOperator::Lt).unwrap(), "\"lt\"");
1289        assert_eq!(serde_json::to_string(&ComparisonOperator::Eq).unwrap(), "\"eq\"");
1290        assert_eq!(serde_json::to_string(&ComparisonOperator::Gte).unwrap(), "\"gte\"");
1291        assert_eq!(serde_json::to_string(&ComparisonOperator::Lte).unwrap(), "\"lte\"");
1292    }
1293}