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: serde_json::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: serde_json::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(crate::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, serde_json::Value>,
344    ) -> Result<serde_json::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(serde_json::Value::String(base.to_uppercase()));
409        }
410        if substituted_str.contains(".toLowerCase()") {
411            let base = substituted_str.replace(".toLowerCase()", "");
412            return Ok(serde_json::Value::String(base.to_lowercase()));
413        }
414        if substituted_str.contains(".trim()") {
415            let base = substituted_str.replace(".trim()", "");
416            return Ok(serde_json::Value::String(base.trim().to_string()));
417        }
418
419        // If no operations detected, return as string
420        Ok(serde_json::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                            serde_json::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                            serde_json::Value::Number(serde_json::Number::from(
677                                num + *amount as i64,
678                            ))
679                        } else {
680                            continue; // Skip non-numeric fields
681                        };
682
683                        let update_query = format!(
684                            "UPDATE {} SET {} = ? WHERE {} = ?",
685                            table_name, field, pk_field
686                        );
687                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
688                    }
689                }
690                MutationOperation::Decrement { field, amount } => {
691                    // Get current value
692                    if let Some(current) = record.get(field) {
693                        let new_value = if let Some(num) = current.as_f64() {
694                            serde_json::Value::Number(
695                                serde_json::Number::from_f64(num - amount)
696                                    .unwrap_or_else(|| serde_json::Number::from(0)),
697                            )
698                        } else if let Some(num) = current.as_i64() {
699                            serde_json::Value::Number(serde_json::Number::from(
700                                num - *amount as i64,
701                            ))
702                        } else {
703                            continue; // Skip non-numeric fields
704                        };
705
706                        let update_query = format!(
707                            "UPDATE {} SET {} = ? WHERE {} = ?",
708                            table_name, field, pk_field
709                        );
710                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
711                    }
712                }
713                MutationOperation::Transform { field, expression } => {
714                    // Evaluate transformation expression
715                    let transformed_value =
716                        Self::evaluate_transformation_expression(expression, &record)?;
717
718                    let update_query =
719                        format!("UPDATE {} SET {} = ? WHERE {} = ?", table_name, field, pk_field);
720                    database.execute(&update_query, &[transformed_value, pk_value.clone()]).await?;
721                }
722                MutationOperation::UpdateStatus { status } => {
723                    let update_query =
724                        format!("UPDATE {} SET status = ? WHERE {} = ?", table_name, pk_field);
725                    database
726                        .execute(
727                            &update_query,
728                            &[serde_json::Value::String(status.clone()), pk_value.clone()],
729                        )
730                        .await?;
731                }
732            }
733        }
734
735        // Update rule state
736        {
737            let mut rules = self.rules.write().await;
738            if let Some(rule) = rules.get_mut(rule_id) {
739                rule.last_execution = Some(now);
740                rule.execution_count += 1;
741
742                // Calculate next execution
743                rule.next_execution = rule.calculate_next_execution(now);
744            }
745        }
746
747        info!("Executed mutation rule '{}' on entity '{}'", rule_id, rule.entity_name);
748        Ok(())
749    }
750}
751
752impl Default for MutationRuleManager {
753    fn default() -> Self {
754        Self::new()
755    }
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    // MutationTrigger tests
763    #[test]
764    fn test_mutation_trigger_interval_serialize() {
765        let trigger = MutationTrigger::Interval {
766            duration_seconds: 3600,
767        };
768        let json = serde_json::to_string(&trigger).unwrap();
769        assert!(json.contains("interval"));
770        assert!(json.contains("3600"));
771    }
772
773    #[test]
774    fn test_mutation_trigger_at_time_serialize() {
775        let trigger = MutationTrigger::AtTime {
776            hour: 9,
777            minute: 30,
778        };
779        let json = serde_json::to_string(&trigger).unwrap();
780        assert!(json.contains("attime"));
781        assert!(json.contains("\"hour\":9"));
782    }
783
784    #[test]
785    fn test_mutation_trigger_field_threshold_serialize() {
786        let trigger = MutationTrigger::FieldThreshold {
787            field: "age".to_string(),
788            threshold: serde_json::json!(100),
789            operator: ComparisonOperator::Gt,
790        };
791        let json = serde_json::to_string(&trigger).unwrap();
792        assert!(json.contains("fieldthreshold"));
793        assert!(json.contains("age"));
794    }
795
796    #[test]
797    fn test_mutation_trigger_clone() {
798        let trigger = MutationTrigger::Interval {
799            duration_seconds: 60,
800        };
801        let cloned = trigger.clone();
802        match cloned {
803            MutationTrigger::Interval { duration_seconds } => {
804                assert_eq!(duration_seconds, 60);
805            }
806            _ => panic!("Expected Interval variant"),
807        }
808    }
809
810    #[test]
811    fn test_mutation_trigger_debug() {
812        let trigger = MutationTrigger::Interval {
813            duration_seconds: 120,
814        };
815        let debug = format!("{:?}", trigger);
816        assert!(debug.contains("Interval"));
817    }
818
819    // MutationOperation tests
820    #[test]
821    fn test_mutation_operation_set() {
822        let op = MutationOperation::Set {
823            field: "status".to_string(),
824            value: serde_json::json!("active"),
825        };
826        let json = serde_json::to_string(&op).unwrap();
827        assert!(json.contains("set"));
828        assert!(json.contains("status"));
829    }
830
831    #[test]
832    fn test_mutation_operation_increment() {
833        let op = MutationOperation::Increment {
834            field: "count".to_string(),
835            amount: 5.0,
836        };
837        let json = serde_json::to_string(&op).unwrap();
838        assert!(json.contains("increment"));
839        assert!(json.contains("count"));
840    }
841
842    #[test]
843    fn test_mutation_operation_decrement() {
844        let op = MutationOperation::Decrement {
845            field: "balance".to_string(),
846            amount: 10.5,
847        };
848        let json = serde_json::to_string(&op).unwrap();
849        assert!(json.contains("decrement"));
850        assert!(json.contains("balance"));
851    }
852
853    #[test]
854    fn test_mutation_operation_transform() {
855        let op = MutationOperation::Transform {
856            field: "value".to_string(),
857            expression: "{{value}} * 2".to_string(),
858        };
859        let json = serde_json::to_string(&op).unwrap();
860        assert!(json.contains("transform"));
861    }
862
863    #[test]
864    fn test_mutation_operation_update_status() {
865        let op = MutationOperation::UpdateStatus {
866            status: "completed".to_string(),
867        };
868        let json = serde_json::to_string(&op).unwrap();
869        assert!(json.contains("updatestatus"));
870        assert!(json.contains("completed"));
871    }
872
873    #[test]
874    fn test_mutation_operation_clone() {
875        let op = MutationOperation::Set {
876            field: "test".to_string(),
877            value: serde_json::json!(42),
878        };
879        let cloned = op.clone();
880        match cloned {
881            MutationOperation::Set { field, value } => {
882                assert_eq!(field, "test");
883                assert_eq!(value, serde_json::json!(42));
884            }
885            _ => panic!("Expected Set variant"),
886        }
887    }
888
889    // MutationRule tests
890    #[test]
891    fn test_mutation_rule_creation() {
892        let rule = MutationRule::new(
893            "test-1".to_string(),
894            "User".to_string(),
895            MutationTrigger::Interval {
896                duration_seconds: 3600,
897            },
898            MutationOperation::Increment {
899                field: "count".to_string(),
900                amount: 1.0,
901            },
902        );
903
904        assert_eq!(rule.id, "test-1");
905        assert_eq!(rule.entity_name, "User");
906        assert!(rule.enabled);
907        assert!(rule.description.is_none());
908        assert!(rule.condition.is_none());
909        assert!(rule.last_execution.is_none());
910        assert_eq!(rule.execution_count, 0);
911    }
912
913    #[test]
914    fn test_mutation_rule_defaults() {
915        let rule = MutationRule::new(
916            "rule-1".to_string(),
917            "Order".to_string(),
918            MutationTrigger::Interval {
919                duration_seconds: 60,
920            },
921            MutationOperation::Set {
922                field: "status".to_string(),
923                value: serde_json::json!("processed"),
924            },
925        );
926
927        // Default values
928        assert!(rule.enabled);
929        assert!(rule.description.is_none());
930        assert!(rule.condition.is_none());
931        assert!(rule.last_execution.is_none());
932        assert_eq!(rule.execution_count, 0);
933    }
934
935    #[test]
936    fn test_mutation_rule_clone() {
937        let rule = MutationRule::new(
938            "clone-test".to_string(),
939            "Entity".to_string(),
940            MutationTrigger::Interval {
941                duration_seconds: 100,
942            },
943            MutationOperation::Increment {
944                field: "counter".to_string(),
945                amount: 1.0,
946            },
947        );
948
949        let cloned = rule.clone();
950        assert_eq!(rule.id, cloned.id);
951        assert_eq!(rule.entity_name, cloned.entity_name);
952        assert_eq!(rule.enabled, cloned.enabled);
953    }
954
955    #[test]
956    fn test_mutation_rule_debug() {
957        let rule = MutationRule::new(
958            "debug-rule".to_string(),
959            "Test".to_string(),
960            MutationTrigger::Interval {
961                duration_seconds: 10,
962            },
963            MutationOperation::Set {
964                field: "f".to_string(),
965                value: serde_json::json!("v"),
966            },
967        );
968
969        let debug = format!("{:?}", rule);
970        assert!(debug.contains("MutationRule"));
971        assert!(debug.contains("debug-rule"));
972    }
973
974    #[test]
975    fn test_mutation_trigger_interval() {
976        let rule = MutationRule::new(
977            "test-1".to_string(),
978            "User".to_string(),
979            MutationTrigger::Interval {
980                duration_seconds: 3600,
981            },
982            MutationOperation::Set {
983                field: "status".to_string(),
984                value: serde_json::json!("active"),
985            },
986        );
987
988        let now = Utc::now();
989        let next = rule.calculate_next_execution(now).unwrap();
990        let duration = next - now;
991
992        // Should be approximately 1 hour
993        assert!(duration.num_seconds() >= 3599 && duration.num_seconds() <= 3601);
994    }
995
996    #[test]
997    fn test_mutation_rule_calculate_next_execution_disabled() {
998        let mut rule = MutationRule::new(
999            "disabled-rule".to_string(),
1000            "Entity".to_string(),
1001            MutationTrigger::Interval {
1002                duration_seconds: 60,
1003            },
1004            MutationOperation::Set {
1005                field: "f".to_string(),
1006                value: serde_json::json!("v"),
1007            },
1008        );
1009        rule.enabled = false;
1010
1011        let now = Utc::now();
1012        assert!(rule.calculate_next_execution(now).is_none());
1013    }
1014
1015    #[test]
1016    fn test_mutation_rule_calculate_next_execution_field_threshold() {
1017        let rule = MutationRule::new(
1018            "threshold-rule".to_string(),
1019            "Entity".to_string(),
1020            MutationTrigger::FieldThreshold {
1021                field: "value".to_string(),
1022                threshold: serde_json::json!(100),
1023                operator: ComparisonOperator::Gt,
1024            },
1025            MutationOperation::Set {
1026                field: "f".to_string(),
1027                value: serde_json::json!("v"),
1028            },
1029        );
1030
1031        // Field threshold triggers don't have scheduled execution
1032        let now = Utc::now();
1033        assert!(rule.calculate_next_execution(now).is_none());
1034    }
1035
1036    // MutationRuleManager tests
1037    #[tokio::test]
1038    async fn test_mutation_rule_manager() {
1039        let manager = MutationRuleManager::new();
1040
1041        let rule = MutationRule::new(
1042            "test-1".to_string(),
1043            "User".to_string(),
1044            MutationTrigger::Interval {
1045                duration_seconds: 3600,
1046            },
1047            MutationOperation::Increment {
1048                field: "count".to_string(),
1049                amount: 1.0,
1050            },
1051        );
1052
1053        manager.add_rule(rule).await.unwrap();
1054
1055        let rules = manager.list_rules().await;
1056        assert_eq!(rules.len(), 1);
1057        assert_eq!(rules[0].id, "test-1");
1058    }
1059
1060    #[test]
1061    fn test_mutation_rule_manager_new() {
1062        let manager = MutationRuleManager::new();
1063        // Manager should be created without error
1064        assert!(true);
1065    }
1066
1067    #[test]
1068    fn test_mutation_rule_manager_default() {
1069        let manager = MutationRuleManager::default();
1070        // Default should work like new
1071        assert!(true);
1072    }
1073
1074    #[tokio::test]
1075    async fn test_mutation_rule_manager_add_and_get_rule() {
1076        let manager = MutationRuleManager::new();
1077
1078        let rule = MutationRule::new(
1079            "get-test".to_string(),
1080            "Order".to_string(),
1081            MutationTrigger::Interval {
1082                duration_seconds: 60,
1083            },
1084            MutationOperation::Set {
1085                field: "status".to_string(),
1086                value: serde_json::json!("done"),
1087            },
1088        );
1089
1090        manager.add_rule(rule).await.unwrap();
1091
1092        let retrieved = manager.get_rule("get-test").await;
1093        assert!(retrieved.is_some());
1094        assert_eq!(retrieved.unwrap().id, "get-test");
1095    }
1096
1097    #[tokio::test]
1098    async fn test_mutation_rule_manager_get_nonexistent() {
1099        let manager = MutationRuleManager::new();
1100        let retrieved = manager.get_rule("nonexistent").await;
1101        assert!(retrieved.is_none());
1102    }
1103
1104    #[tokio::test]
1105    async fn test_mutation_rule_manager_remove_rule() {
1106        let manager = MutationRuleManager::new();
1107
1108        let rule = MutationRule::new(
1109            "remove-test".to_string(),
1110            "Entity".to_string(),
1111            MutationTrigger::Interval {
1112                duration_seconds: 60,
1113            },
1114            MutationOperation::Set {
1115                field: "f".to_string(),
1116                value: serde_json::json!("v"),
1117            },
1118        );
1119
1120        manager.add_rule(rule).await.unwrap();
1121        assert!(manager.get_rule("remove-test").await.is_some());
1122
1123        let removed = manager.remove_rule("remove-test").await;
1124        assert!(removed);
1125        assert!(manager.get_rule("remove-test").await.is_none());
1126    }
1127
1128    #[tokio::test]
1129    async fn test_mutation_rule_manager_remove_nonexistent() {
1130        let manager = MutationRuleManager::new();
1131        let removed = manager.remove_rule("nonexistent").await;
1132        assert!(!removed);
1133    }
1134
1135    #[tokio::test]
1136    async fn test_mutation_rule_manager_list_rules() {
1137        let manager = MutationRuleManager::new();
1138
1139        // Add multiple rules
1140        for i in 1..=3 {
1141            let rule = MutationRule::new(
1142                format!("rule-{}", i),
1143                "Entity".to_string(),
1144                MutationTrigger::Interval {
1145                    duration_seconds: 60,
1146                },
1147                MutationOperation::Set {
1148                    field: "f".to_string(),
1149                    value: serde_json::json!("v"),
1150                },
1151            );
1152            manager.add_rule(rule).await.unwrap();
1153        }
1154
1155        let rules = manager.list_rules().await;
1156        assert_eq!(rules.len(), 3);
1157    }
1158
1159    #[tokio::test]
1160    async fn test_mutation_rule_manager_list_rules_for_entity() {
1161        let manager = MutationRuleManager::new();
1162
1163        // Add rules for different entities
1164        let rule1 = MutationRule::new(
1165            "user-rule".to_string(),
1166            "User".to_string(),
1167            MutationTrigger::Interval {
1168                duration_seconds: 60,
1169            },
1170            MutationOperation::Set {
1171                field: "f".to_string(),
1172                value: serde_json::json!("v"),
1173            },
1174        );
1175
1176        let rule2 = MutationRule::new(
1177            "order-rule".to_string(),
1178            "Order".to_string(),
1179            MutationTrigger::Interval {
1180                duration_seconds: 60,
1181            },
1182            MutationOperation::Set {
1183                field: "f".to_string(),
1184                value: serde_json::json!("v"),
1185            },
1186        );
1187
1188        let rule3 = MutationRule::new(
1189            "user-rule-2".to_string(),
1190            "User".to_string(),
1191            MutationTrigger::Interval {
1192                duration_seconds: 120,
1193            },
1194            MutationOperation::Increment {
1195                field: "count".to_string(),
1196                amount: 1.0,
1197            },
1198        );
1199
1200        manager.add_rule(rule1).await.unwrap();
1201        manager.add_rule(rule2).await.unwrap();
1202        manager.add_rule(rule3).await.unwrap();
1203
1204        let user_rules = manager.list_rules_for_entity("User").await;
1205        assert_eq!(user_rules.len(), 2);
1206
1207        let order_rules = manager.list_rules_for_entity("Order").await;
1208        assert_eq!(order_rules.len(), 1);
1209
1210        let product_rules = manager.list_rules_for_entity("Product").await;
1211        assert!(product_rules.is_empty());
1212    }
1213
1214    #[tokio::test]
1215    async fn test_mutation_rule_manager_set_rule_enabled() {
1216        let manager = MutationRuleManager::new();
1217
1218        let rule = MutationRule::new(
1219            "enable-test".to_string(),
1220            "Entity".to_string(),
1221            MutationTrigger::Interval {
1222                duration_seconds: 60,
1223            },
1224            MutationOperation::Set {
1225                field: "f".to_string(),
1226                value: serde_json::json!("v"),
1227            },
1228        );
1229
1230        manager.add_rule(rule).await.unwrap();
1231
1232        // Disable the rule
1233        manager.set_rule_enabled("enable-test", false).await.unwrap();
1234        let disabled_rule = manager.get_rule("enable-test").await.unwrap();
1235        assert!(!disabled_rule.enabled);
1236        assert!(disabled_rule.next_execution.is_none());
1237
1238        // Re-enable the rule
1239        manager.set_rule_enabled("enable-test", true).await.unwrap();
1240        let enabled_rule = manager.get_rule("enable-test").await.unwrap();
1241        assert!(enabled_rule.enabled);
1242        assert!(enabled_rule.next_execution.is_some());
1243    }
1244
1245    #[tokio::test]
1246    async fn test_mutation_rule_manager_set_rule_enabled_nonexistent() {
1247        let manager = MutationRuleManager::new();
1248        let result = manager.set_rule_enabled("nonexistent", true).await;
1249        assert!(result.is_err());
1250    }
1251
1252    // ComparisonOperator tests
1253    #[test]
1254    fn test_comparison_operator_variants() {
1255        let operators = vec![
1256            ComparisonOperator::Gt,
1257            ComparisonOperator::Lt,
1258            ComparisonOperator::Eq,
1259            ComparisonOperator::Gte,
1260            ComparisonOperator::Lte,
1261        ];
1262
1263        for op in operators {
1264            let json = serde_json::to_string(&op).unwrap();
1265            assert!(!json.is_empty());
1266        }
1267    }
1268
1269    #[test]
1270    fn test_comparison_operator_clone() {
1271        let op = ComparisonOperator::Gt;
1272        let cloned = op.clone();
1273        assert!(matches!(cloned, ComparisonOperator::Gt));
1274    }
1275
1276    #[test]
1277    fn test_comparison_operator_debug() {
1278        let op = ComparisonOperator::Lt;
1279        let debug = format!("{:?}", op);
1280        assert!(debug.contains("Lt"));
1281    }
1282
1283    #[test]
1284    fn test_comparison_operator_eq() {
1285        let op1 = ComparisonOperator::Gt;
1286        let op2 = ComparisonOperator::Gt;
1287        let op3 = ComparisonOperator::Lt;
1288        assert_eq!(op1, op2);
1289        assert_ne!(op1, op3);
1290    }
1291
1292    #[test]
1293    fn test_comparison_operator_serialize() {
1294        assert_eq!(serde_json::to_string(&ComparisonOperator::Gt).unwrap(), "\"gt\"");
1295        assert_eq!(serde_json::to_string(&ComparisonOperator::Lt).unwrap(), "\"lt\"");
1296        assert_eq!(serde_json::to_string(&ComparisonOperator::Eq).unwrap(), "\"eq\"");
1297        assert_eq!(serde_json::to_string(&ComparisonOperator::Gte).unwrap(), "\"gte\"");
1298        assert_eq!(serde_json::to_string(&ComparisonOperator::Lte).unwrap(), "\"lte\"");
1299    }
1300}