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::internal(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::internal(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::internal(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::internal("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::internal(format!(
470                        "Invalid character in expression: {}",
471                        ch
472                    )));
473                }
474            }
475        }
476
477        // Handle last number
478        if !current_num.is_empty() {
479            let num: f64 = current_num
480                .parse()
481                .map_err(|_| Error::internal(format!("Invalid number: {}", current_num)))?;
482
483            match last_op {
484                '+' => result += num,
485                '-' => result -= num,
486                '*' => result *= num,
487                '/' => {
488                    if num == 0.0 {
489                        return Err(Error::internal("Division by zero".to_string()));
490                    }
491                    result /= num;
492                }
493                _ => result = num, // First number
494            }
495        }
496
497        Ok(result)
498    }
499
500    /// Evaluate a JSONPath condition against a record
501    ///
502    /// The condition can be:
503    /// - A simple JSONPath expression that checks for existence (e.g., "$.status")
504    /// - A JSONPath expression with comparison (e.g., "$.status == 'active'")
505    /// - A boolean JSONPath expression (e.g., "$.enabled")
506    ///
507    /// Returns true if the condition is met, false otherwise.
508    fn evaluate_condition(condition: &str, record: &Value) -> Result<bool> {
509        // Simple JSONPath evaluation
510        // For basic existence checks (e.g., "$.field"), check if path exists and is truthy
511        // For comparison expressions (e.g., "$.field == 'value'"), parse and evaluate
512
513        // Try to parse as JSONPath selector
514        if let Ok(selector) = jsonpath::Selector::new(condition) {
515            // If condition is just a path (no comparison), check if it exists and is truthy
516            let results: Vec<_> = selector.find(record).collect();
517            if !results.is_empty() {
518                // Check if any result is truthy
519                for result in results {
520                    match result {
521                        Value::Bool(b) => {
522                            if *b {
523                                return Ok(true);
524                            }
525                        }
526                        Value::Null => continue,
527                        Value::String(s) => {
528                            if !s.is_empty() {
529                                return Ok(true);
530                            }
531                        }
532                        Value::Number(n) => {
533                            if n.as_f64().map(|f| f != 0.0).unwrap_or(false) {
534                                return Ok(true);
535                            }
536                        }
537                        _ => return Ok(true), // Other types (objects, arrays) are truthy
538                    }
539                }
540            }
541            return Ok(false);
542        }
543
544        // If JSONPath parsing fails, try to parse as a comparison expression
545        // Simple pattern: "$.field == 'value'" or "$.field > 10"
546        if condition.contains("==") {
547            let parts: Vec<&str> = condition.split("==").map(|s| s.trim()).collect();
548            if parts.len() == 2 {
549                let path = parts[0].trim();
550                let expected = parts[1].trim().trim_matches('\'').trim_matches('"');
551
552                if let Ok(selector) = jsonpath::Selector::new(path) {
553                    let results: Vec<_> = selector.find(record).collect();
554                    for result in results {
555                        match result {
556                            Value::String(s) if s == expected => return Ok(true),
557                            Value::Number(n) => {
558                                if let Ok(expected_num) = expected.parse::<f64>() {
559                                    if n.as_f64().map(|f| f == expected_num).unwrap_or(false) {
560                                        return Ok(true);
561                                    }
562                                }
563                            }
564                            _ => {}
565                        }
566                    }
567                }
568            }
569        } else if condition.contains(">") {
570            let parts: Vec<&str> = condition.split(">").map(|s| s.trim()).collect();
571            if parts.len() == 2 {
572                let path = parts[0].trim();
573                if let Ok(expected_num) = parts[1].trim().parse::<f64>() {
574                    if let Ok(selector) = jsonpath::Selector::new(path) {
575                        let results: Vec<_> = selector.find(record).collect();
576                        for result in results {
577                            if let Value::Number(n) = result {
578                                if n.as_f64().map(|f| f > expected_num).unwrap_or(false) {
579                                    return Ok(true);
580                                }
581                            }
582                        }
583                    }
584                }
585            }
586        } else if condition.contains("<") {
587            let parts: Vec<&str> = condition.split("<").map(|s| s.trim()).collect();
588            if parts.len() == 2 {
589                let path = parts[0].trim();
590                if let Ok(expected_num) = parts[1].trim().parse::<f64>() {
591                    if let Ok(selector) = jsonpath::Selector::new(path) {
592                        let results: Vec<_> = selector.find(record).collect();
593                        for result in results {
594                            if let Value::Number(n) = result {
595                                if n.as_f64().map(|f| f < expected_num).unwrap_or(false) {
596                                    return Ok(true);
597                                }
598                            }
599                        }
600                    }
601                }
602            }
603        }
604
605        // If we can't parse the condition, log a warning and return false
606        warn!("Could not evaluate condition '{}', treating as false", condition);
607        Ok(false)
608    }
609
610    /// Execute a specific mutation rule
611    async fn execute_rule(
612        &self,
613        rule_id: &str,
614        database: &dyn crate::database::VirtualDatabase,
615        registry: &crate::entities::EntityRegistry,
616    ) -> Result<()> {
617        let now = time_travel_now();
618
619        // Get rule
620        let rule = {
621            let rules = self.rules.read().await;
622            rules
623                .get(rule_id)
624                .ok_or_else(|| Error::internal(format!("Mutation rule '{}' not found", rule_id)))?
625                .clone()
626        };
627
628        // Get entity info
629        let entity = registry
630            .get(&rule.entity_name)
631            .ok_or_else(|| Error::internal(format!("Entity '{}' not found", rule.entity_name)))?;
632
633        let table_name = entity.table_name();
634
635        // Query all records for this entity
636        let query = format!("SELECT * FROM {}", table_name);
637        let records = database.query(&query, &[]).await?;
638
639        // Apply mutation to each record
640        let pk_field = entity.schema.primary_key.first().map(|s| s.as_str()).unwrap_or("id");
641
642        for record in records {
643            // Check condition if specified
644            if let Some(ref condition) = rule.condition {
645                // Convert HashMap to Value for JSONPath evaluation
646                let record_value =
647                    Value::Object(record.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
648
649                // Evaluate JSONPath condition
650                // Condition should be a JSONPath expression that evaluates to a truthy value
651                // Examples: "$.status == 'active'", "$.age > 18", "$.enabled"
652                if !MutationRuleManager::evaluate_condition(condition, &record_value)? {
653                    debug!("Condition '{}' not met for record, skipping", condition);
654                    continue;
655                }
656            }
657
658            // Get primary key value
659            let pk_value = record
660                .get(pk_field)
661                .ok_or_else(|| Error::internal(format!("Primary key '{}' not found", pk_field)))?;
662
663            // Apply mutation operation
664            match &rule.operation {
665                MutationOperation::Set { field, value } => {
666                    let update_query =
667                        format!("UPDATE {} SET {} = ? WHERE {} = ?", table_name, field, pk_field);
668                    database.execute(&update_query, &[value.clone(), pk_value.clone()]).await?;
669                }
670                MutationOperation::Increment { field, amount } => {
671                    // Get current value
672                    if let Some(current) = record.get(field) {
673                        let new_value = if let Some(num) = current.as_f64() {
674                            Value::Number(
675                                serde_json::Number::from_f64(num + amount)
676                                    .unwrap_or_else(|| serde_json::Number::from(0)),
677                            )
678                        } else if let Some(num) = current.as_i64() {
679                            Value::Number(serde_json::Number::from(num + *amount as i64))
680                        } else {
681                            continue; // Skip non-numeric fields
682                        };
683
684                        let update_query = format!(
685                            "UPDATE {} SET {} = ? WHERE {} = ?",
686                            table_name, field, pk_field
687                        );
688                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
689                    }
690                }
691                MutationOperation::Decrement { field, amount } => {
692                    // Get current value
693                    if let Some(current) = record.get(field) {
694                        let new_value = if let Some(num) = current.as_f64() {
695                            Value::Number(
696                                serde_json::Number::from_f64(num - amount)
697                                    .unwrap_or_else(|| serde_json::Number::from(0)),
698                            )
699                        } else if let Some(num) = current.as_i64() {
700                            Value::Number(serde_json::Number::from(num - *amount as i64))
701                        } else {
702                            continue; // Skip non-numeric fields
703                        };
704
705                        let update_query = format!(
706                            "UPDATE {} SET {} = ? WHERE {} = ?",
707                            table_name, field, pk_field
708                        );
709                        database.execute(&update_query, &[new_value, pk_value.clone()]).await?;
710                    }
711                }
712                MutationOperation::Transform { field, expression } => {
713                    // Evaluate transformation expression
714                    let transformed_value =
715                        Self::evaluate_transformation_expression(expression, &record)?;
716
717                    let update_query =
718                        format!("UPDATE {} SET {} = ? WHERE {} = ?", table_name, field, pk_field);
719                    database.execute(&update_query, &[transformed_value, pk_value.clone()]).await?;
720                }
721                MutationOperation::UpdateStatus { status } => {
722                    let update_query =
723                        format!("UPDATE {} SET status = ? WHERE {} = ?", table_name, pk_field);
724                    database
725                        .execute(&update_query, &[Value::String(status.clone()), pk_value.clone()])
726                        .await?;
727                }
728            }
729        }
730
731        // Update rule state
732        {
733            let mut rules = self.rules.write().await;
734            if let Some(rule) = rules.get_mut(rule_id) {
735                rule.last_execution = Some(now);
736                rule.execution_count += 1;
737
738                // Calculate next execution
739                rule.next_execution = rule.calculate_next_execution(now);
740            }
741        }
742
743        info!("Executed mutation rule '{}' on entity '{}'", rule_id, rule.entity_name);
744        Ok(())
745    }
746}
747
748impl Default for MutationRuleManager {
749    fn default() -> Self {
750        Self::new()
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    // MutationTrigger tests
759    #[test]
760    fn test_mutation_trigger_interval_serialize() {
761        let trigger = MutationTrigger::Interval {
762            duration_seconds: 3600,
763        };
764        let json = serde_json::to_string(&trigger).unwrap();
765        assert!(json.contains("interval"));
766        assert!(json.contains("3600"));
767    }
768
769    #[test]
770    fn test_mutation_trigger_at_time_serialize() {
771        let trigger = MutationTrigger::AtTime {
772            hour: 9,
773            minute: 30,
774        };
775        let json = serde_json::to_string(&trigger).unwrap();
776        assert!(json.contains("attime"));
777        assert!(json.contains("\"hour\":9"));
778    }
779
780    #[test]
781    fn test_mutation_trigger_field_threshold_serialize() {
782        let trigger = MutationTrigger::FieldThreshold {
783            field: "age".to_string(),
784            threshold: serde_json::json!(100),
785            operator: ComparisonOperator::Gt,
786        };
787        let json = serde_json::to_string(&trigger).unwrap();
788        assert!(json.contains("fieldthreshold"));
789        assert!(json.contains("age"));
790    }
791
792    #[test]
793    fn test_mutation_trigger_clone() {
794        let trigger = MutationTrigger::Interval {
795            duration_seconds: 60,
796        };
797        let cloned = trigger.clone();
798        match cloned {
799            MutationTrigger::Interval { duration_seconds } => {
800                assert_eq!(duration_seconds, 60);
801            }
802            _ => panic!("Expected Interval variant"),
803        }
804    }
805
806    #[test]
807    fn test_mutation_trigger_debug() {
808        let trigger = MutationTrigger::Interval {
809            duration_seconds: 120,
810        };
811        let debug = format!("{:?}", trigger);
812        assert!(debug.contains("Interval"));
813    }
814
815    // MutationOperation tests
816    #[test]
817    fn test_mutation_operation_set() {
818        let op = MutationOperation::Set {
819            field: "status".to_string(),
820            value: serde_json::json!("active"),
821        };
822        let json = serde_json::to_string(&op).unwrap();
823        assert!(json.contains("set"));
824        assert!(json.contains("status"));
825    }
826
827    #[test]
828    fn test_mutation_operation_increment() {
829        let op = MutationOperation::Increment {
830            field: "count".to_string(),
831            amount: 5.0,
832        };
833        let json = serde_json::to_string(&op).unwrap();
834        assert!(json.contains("increment"));
835        assert!(json.contains("count"));
836    }
837
838    #[test]
839    fn test_mutation_operation_decrement() {
840        let op = MutationOperation::Decrement {
841            field: "balance".to_string(),
842            amount: 10.5,
843        };
844        let json = serde_json::to_string(&op).unwrap();
845        assert!(json.contains("decrement"));
846        assert!(json.contains("balance"));
847    }
848
849    #[test]
850    fn test_mutation_operation_transform() {
851        let op = MutationOperation::Transform {
852            field: "value".to_string(),
853            expression: "{{value}} * 2".to_string(),
854        };
855        let json = serde_json::to_string(&op).unwrap();
856        assert!(json.contains("transform"));
857    }
858
859    #[test]
860    fn test_mutation_operation_update_status() {
861        let op = MutationOperation::UpdateStatus {
862            status: "completed".to_string(),
863        };
864        let json = serde_json::to_string(&op).unwrap();
865        assert!(json.contains("updatestatus"));
866        assert!(json.contains("completed"));
867    }
868
869    #[test]
870    fn test_mutation_operation_clone() {
871        let op = MutationOperation::Set {
872            field: "test".to_string(),
873            value: serde_json::json!(42),
874        };
875        let cloned = op.clone();
876        match cloned {
877            MutationOperation::Set { field, value } => {
878                assert_eq!(field, "test");
879                assert_eq!(value, serde_json::json!(42));
880            }
881            _ => panic!("Expected Set variant"),
882        }
883    }
884
885    // MutationRule tests
886    #[test]
887    fn test_mutation_rule_creation() {
888        let rule = MutationRule::new(
889            "test-1".to_string(),
890            "User".to_string(),
891            MutationTrigger::Interval {
892                duration_seconds: 3600,
893            },
894            MutationOperation::Increment {
895                field: "count".to_string(),
896                amount: 1.0,
897            },
898        );
899
900        assert_eq!(rule.id, "test-1");
901        assert_eq!(rule.entity_name, "User");
902        assert!(rule.enabled);
903        assert!(rule.description.is_none());
904        assert!(rule.condition.is_none());
905        assert!(rule.last_execution.is_none());
906        assert_eq!(rule.execution_count, 0);
907    }
908
909    #[test]
910    fn test_mutation_rule_defaults() {
911        let rule = MutationRule::new(
912            "rule-1".to_string(),
913            "Order".to_string(),
914            MutationTrigger::Interval {
915                duration_seconds: 60,
916            },
917            MutationOperation::Set {
918                field: "status".to_string(),
919                value: serde_json::json!("processed"),
920            },
921        );
922
923        // Default values
924        assert!(rule.enabled);
925        assert!(rule.description.is_none());
926        assert!(rule.condition.is_none());
927        assert!(rule.last_execution.is_none());
928        assert_eq!(rule.execution_count, 0);
929    }
930
931    #[test]
932    fn test_mutation_rule_clone() {
933        let rule = MutationRule::new(
934            "clone-test".to_string(),
935            "Entity".to_string(),
936            MutationTrigger::Interval {
937                duration_seconds: 100,
938            },
939            MutationOperation::Increment {
940                field: "counter".to_string(),
941                amount: 1.0,
942            },
943        );
944
945        let cloned = rule.clone();
946        assert_eq!(rule.id, cloned.id);
947        assert_eq!(rule.entity_name, cloned.entity_name);
948        assert_eq!(rule.enabled, cloned.enabled);
949    }
950
951    #[test]
952    fn test_mutation_rule_debug() {
953        let rule = MutationRule::new(
954            "debug-rule".to_string(),
955            "Test".to_string(),
956            MutationTrigger::Interval {
957                duration_seconds: 10,
958            },
959            MutationOperation::Set {
960                field: "f".to_string(),
961                value: serde_json::json!("v"),
962            },
963        );
964
965        let debug = format!("{:?}", rule);
966        assert!(debug.contains("MutationRule"));
967        assert!(debug.contains("debug-rule"));
968    }
969
970    #[test]
971    fn test_mutation_trigger_interval() {
972        let rule = MutationRule::new(
973            "test-1".to_string(),
974            "User".to_string(),
975            MutationTrigger::Interval {
976                duration_seconds: 3600,
977            },
978            MutationOperation::Set {
979                field: "status".to_string(),
980                value: serde_json::json!("active"),
981            },
982        );
983
984        let now = Utc::now();
985        let next = rule.calculate_next_execution(now).unwrap();
986        let duration = next - now;
987
988        // Should be approximately 1 hour
989        assert!(duration.num_seconds() >= 3599 && duration.num_seconds() <= 3601);
990    }
991
992    #[test]
993    fn test_mutation_rule_calculate_next_execution_disabled() {
994        let mut rule = MutationRule::new(
995            "disabled-rule".to_string(),
996            "Entity".to_string(),
997            MutationTrigger::Interval {
998                duration_seconds: 60,
999            },
1000            MutationOperation::Set {
1001                field: "f".to_string(),
1002                value: serde_json::json!("v"),
1003            },
1004        );
1005        rule.enabled = false;
1006
1007        let now = Utc::now();
1008        assert!(rule.calculate_next_execution(now).is_none());
1009    }
1010
1011    #[test]
1012    fn test_mutation_rule_calculate_next_execution_field_threshold() {
1013        let rule = MutationRule::new(
1014            "threshold-rule".to_string(),
1015            "Entity".to_string(),
1016            MutationTrigger::FieldThreshold {
1017                field: "value".to_string(),
1018                threshold: serde_json::json!(100),
1019                operator: ComparisonOperator::Gt,
1020            },
1021            MutationOperation::Set {
1022                field: "f".to_string(),
1023                value: serde_json::json!("v"),
1024            },
1025        );
1026
1027        // Field threshold triggers don't have scheduled execution
1028        let now = Utc::now();
1029        assert!(rule.calculate_next_execution(now).is_none());
1030    }
1031
1032    // MutationRuleManager tests
1033    #[tokio::test]
1034    async fn test_mutation_rule_manager() {
1035        let manager = MutationRuleManager::new();
1036
1037        let rule = MutationRule::new(
1038            "test-1".to_string(),
1039            "User".to_string(),
1040            MutationTrigger::Interval {
1041                duration_seconds: 3600,
1042            },
1043            MutationOperation::Increment {
1044                field: "count".to_string(),
1045                amount: 1.0,
1046            },
1047        );
1048
1049        manager.add_rule(rule).await.unwrap();
1050
1051        let rules = manager.list_rules().await;
1052        assert_eq!(rules.len(), 1);
1053        assert_eq!(rules[0].id, "test-1");
1054    }
1055
1056    #[tokio::test]
1057    async fn test_mutation_rule_manager_new() {
1058        let manager = MutationRuleManager::new();
1059        let rules = manager.list_rules().await;
1060        assert!(rules.is_empty());
1061    }
1062
1063    #[tokio::test]
1064    async fn test_mutation_rule_manager_default() {
1065        let manager = MutationRuleManager::default();
1066        let rules = manager.list_rules().await;
1067        assert!(rules.is_empty());
1068    }
1069
1070    #[tokio::test]
1071    async fn test_mutation_rule_manager_add_and_get_rule() {
1072        let manager = MutationRuleManager::new();
1073
1074        let rule = MutationRule::new(
1075            "get-test".to_string(),
1076            "Order".to_string(),
1077            MutationTrigger::Interval {
1078                duration_seconds: 60,
1079            },
1080            MutationOperation::Set {
1081                field: "status".to_string(),
1082                value: serde_json::json!("done"),
1083            },
1084        );
1085
1086        manager.add_rule(rule).await.unwrap();
1087
1088        let retrieved = manager.get_rule("get-test").await;
1089        assert!(retrieved.is_some());
1090        assert_eq!(retrieved.unwrap().id, "get-test");
1091    }
1092
1093    #[tokio::test]
1094    async fn test_mutation_rule_manager_get_nonexistent() {
1095        let manager = MutationRuleManager::new();
1096        let retrieved = manager.get_rule("nonexistent").await;
1097        assert!(retrieved.is_none());
1098    }
1099
1100    #[tokio::test]
1101    async fn test_mutation_rule_manager_remove_rule() {
1102        let manager = MutationRuleManager::new();
1103
1104        let rule = MutationRule::new(
1105            "remove-test".to_string(),
1106            "Entity".to_string(),
1107            MutationTrigger::Interval {
1108                duration_seconds: 60,
1109            },
1110            MutationOperation::Set {
1111                field: "f".to_string(),
1112                value: serde_json::json!("v"),
1113            },
1114        );
1115
1116        manager.add_rule(rule).await.unwrap();
1117        assert!(manager.get_rule("remove-test").await.is_some());
1118
1119        let removed = manager.remove_rule("remove-test").await;
1120        assert!(removed);
1121        assert!(manager.get_rule("remove-test").await.is_none());
1122    }
1123
1124    #[tokio::test]
1125    async fn test_mutation_rule_manager_remove_nonexistent() {
1126        let manager = MutationRuleManager::new();
1127        let removed = manager.remove_rule("nonexistent").await;
1128        assert!(!removed);
1129    }
1130
1131    #[tokio::test]
1132    async fn test_mutation_rule_manager_list_rules() {
1133        let manager = MutationRuleManager::new();
1134
1135        // Add multiple rules
1136        for i in 1..=3 {
1137            let rule = MutationRule::new(
1138                format!("rule-{}", i),
1139                "Entity".to_string(),
1140                MutationTrigger::Interval {
1141                    duration_seconds: 60,
1142                },
1143                MutationOperation::Set {
1144                    field: "f".to_string(),
1145                    value: serde_json::json!("v"),
1146                },
1147            );
1148            manager.add_rule(rule).await.unwrap();
1149        }
1150
1151        let rules = manager.list_rules().await;
1152        assert_eq!(rules.len(), 3);
1153    }
1154
1155    #[tokio::test]
1156    async fn test_mutation_rule_manager_list_rules_for_entity() {
1157        let manager = MutationRuleManager::new();
1158
1159        // Add rules for different entities
1160        let rule1 = MutationRule::new(
1161            "user-rule".to_string(),
1162            "User".to_string(),
1163            MutationTrigger::Interval {
1164                duration_seconds: 60,
1165            },
1166            MutationOperation::Set {
1167                field: "f".to_string(),
1168                value: serde_json::json!("v"),
1169            },
1170        );
1171
1172        let rule2 = MutationRule::new(
1173            "order-rule".to_string(),
1174            "Order".to_string(),
1175            MutationTrigger::Interval {
1176                duration_seconds: 60,
1177            },
1178            MutationOperation::Set {
1179                field: "f".to_string(),
1180                value: serde_json::json!("v"),
1181            },
1182        );
1183
1184        let rule3 = MutationRule::new(
1185            "user-rule-2".to_string(),
1186            "User".to_string(),
1187            MutationTrigger::Interval {
1188                duration_seconds: 120,
1189            },
1190            MutationOperation::Increment {
1191                field: "count".to_string(),
1192                amount: 1.0,
1193            },
1194        );
1195
1196        manager.add_rule(rule1).await.unwrap();
1197        manager.add_rule(rule2).await.unwrap();
1198        manager.add_rule(rule3).await.unwrap();
1199
1200        let user_rules = manager.list_rules_for_entity("User").await;
1201        assert_eq!(user_rules.len(), 2);
1202
1203        let order_rules = manager.list_rules_for_entity("Order").await;
1204        assert_eq!(order_rules.len(), 1);
1205
1206        let product_rules = manager.list_rules_for_entity("Product").await;
1207        assert!(product_rules.is_empty());
1208    }
1209
1210    #[tokio::test]
1211    async fn test_mutation_rule_manager_set_rule_enabled() {
1212        let manager = MutationRuleManager::new();
1213
1214        let rule = MutationRule::new(
1215            "enable-test".to_string(),
1216            "Entity".to_string(),
1217            MutationTrigger::Interval {
1218                duration_seconds: 60,
1219            },
1220            MutationOperation::Set {
1221                field: "f".to_string(),
1222                value: serde_json::json!("v"),
1223            },
1224        );
1225
1226        manager.add_rule(rule).await.unwrap();
1227
1228        // Disable the rule
1229        manager.set_rule_enabled("enable-test", false).await.unwrap();
1230        let disabled_rule = manager.get_rule("enable-test").await.unwrap();
1231        assert!(!disabled_rule.enabled);
1232        assert!(disabled_rule.next_execution.is_none());
1233
1234        // Re-enable the rule
1235        manager.set_rule_enabled("enable-test", true).await.unwrap();
1236        let enabled_rule = manager.get_rule("enable-test").await.unwrap();
1237        assert!(enabled_rule.enabled);
1238        assert!(enabled_rule.next_execution.is_some());
1239    }
1240
1241    #[tokio::test]
1242    async fn test_mutation_rule_manager_set_rule_enabled_nonexistent() {
1243        let manager = MutationRuleManager::new();
1244        let result = manager.set_rule_enabled("nonexistent", true).await;
1245        assert!(result.is_err());
1246    }
1247
1248    // ComparisonOperator tests
1249    #[test]
1250    fn test_comparison_operator_variants() {
1251        let operators = vec![
1252            ComparisonOperator::Gt,
1253            ComparisonOperator::Lt,
1254            ComparisonOperator::Eq,
1255            ComparisonOperator::Gte,
1256            ComparisonOperator::Lte,
1257        ];
1258
1259        for op in operators {
1260            let json = serde_json::to_string(&op).unwrap();
1261            assert!(!json.is_empty());
1262        }
1263    }
1264
1265    #[test]
1266    fn test_comparison_operator_clone() {
1267        let op = ComparisonOperator::Gt;
1268        let cloned = op;
1269        assert!(matches!(cloned, ComparisonOperator::Gt));
1270    }
1271
1272    #[test]
1273    fn test_comparison_operator_debug() {
1274        let op = ComparisonOperator::Lt;
1275        let debug = format!("{:?}", op);
1276        assert!(debug.contains("Lt"));
1277    }
1278
1279    #[test]
1280    fn test_comparison_operator_eq() {
1281        let op1 = ComparisonOperator::Gt;
1282        let op2 = ComparisonOperator::Gt;
1283        let op3 = ComparisonOperator::Lt;
1284        assert_eq!(op1, op2);
1285        assert_ne!(op1, op3);
1286    }
1287
1288    #[test]
1289    fn test_comparison_operator_serialize() {
1290        assert_eq!(serde_json::to_string(&ComparisonOperator::Gt).unwrap(), "\"gt\"");
1291        assert_eq!(serde_json::to_string(&ComparisonOperator::Lt).unwrap(), "\"lt\"");
1292        assert_eq!(serde_json::to_string(&ComparisonOperator::Eq).unwrap(), "\"eq\"");
1293        assert_eq!(serde_json::to_string(&ComparisonOperator::Gte).unwrap(), "\"gte\"");
1294        assert_eq!(serde_json::to_string(&ComparisonOperator::Lte).unwrap(), "\"lte\"");
1295    }
1296}