Skip to main content

oxidize_pdf/forms/
calculation_system.rs

1//! Enhanced form calculation system with JavaScript support
2//!
3//! This module provides a complete calculation system for PDF forms supporting:
4//! - JavaScript calculations (AFSimple, AFPercent, AFDate)
5//! - Field dependencies and automatic recalculation
6//! - Calculation order management
7//! - Format validation
8
9use crate::error::PdfError;
10use crate::forms::calculations::{CalculationEngine, FieldValue};
11use crate::objects::{Dictionary, Object};
12use chrono::{DateTime, NaiveDate, Utc};
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::fmt;
15
16/// Complete calculation system for PDF forms
17#[derive(Debug, Clone)]
18pub struct FormCalculationSystem {
19    /// Core calculation engine
20    engine: CalculationEngine,
21    /// JavaScript calculations
22    js_calculations: HashMap<String, JavaScriptCalculation>,
23    /// Field formats
24    field_formats: HashMap<String, FieldFormat>,
25    /// Calculation events
26    events: Vec<CalculationEvent>,
27    /// Settings
28    settings: CalculationSettings,
29}
30
31/// JavaScript calculation types (Adobe Forms)
32#[derive(Debug, Clone)]
33pub enum JavaScriptCalculation {
34    /// AFSimple_Calculate - Basic arithmetic operations
35    SimpleCalculate {
36        operation: SimpleOperation,
37        fields: Vec<String>,
38    },
39    /// AFPercent_Calculate - Percentage calculations
40    PercentCalculate {
41        base_field: String,
42        percent_field: String,
43        mode: PercentMode,
44    },
45    /// AFDate_Calculate - Date calculations
46    DateCalculate {
47        start_date_field: String,
48        days_field: Option<String>,
49        format: String,
50    },
51    /// AFRange_Calculate - Range validation
52    RangeCalculate {
53        field: String,
54        min: Option<f64>,
55        max: Option<f64>,
56    },
57    /// AFNumber_Calculate - Number formatting
58    NumberCalculate {
59        field: String,
60        decimals: usize,
61        sep_style: SeparatorStyle,
62        currency: Option<String>,
63    },
64    /// Custom JavaScript code
65    Custom {
66        script: String,
67        dependencies: Vec<String>,
68    },
69}
70
71/// Simple arithmetic operations for AFSimple_Calculate
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum SimpleOperation {
74    Sum,     // SUM
75    Product, // PRD
76    Average, // AVG
77    Minimum, // MIN
78    Maximum, // MAX
79}
80
81/// Percentage calculation modes
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum PercentMode {
84    /// Calculate X% of base
85    PercentOf,
86    /// Calculate what % X is of base
87    PercentageOf,
88    /// Add X% to base
89    AddPercent,
90    /// Subtract X% from base
91    SubtractPercent,
92}
93
94/// Number separator styles
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub enum SeparatorStyle {
97    /// 1,234.56
98    CommaPeriod,
99    /// 1.234,56
100    PeriodComma,
101    /// 1 234.56
102    SpacePeriod,
103    /// 1'234.56
104    ApostrophePeriod,
105    /// 1234.56
106    None,
107}
108
109/// Field format specification
110#[derive(Debug, Clone)]
111pub enum FieldFormat {
112    /// Number format
113    Number {
114        decimals: usize,
115        separator: SeparatorStyle,
116        negative_style: NegativeStyle,
117        currency: Option<String>,
118    },
119    /// Percentage format
120    Percent { decimals: usize },
121    /// Date format
122    Date { format: String },
123    /// Time format
124    Time { format: String },
125    /// Special format (SSN, Phone, Zip)
126    Special { format_type: SpecialFormat },
127    /// Custom format
128    Custom { format_string: String },
129}
130
131/// Negative number display styles
132#[derive(Debug, Clone, Copy, PartialEq)]
133pub enum NegativeStyle {
134    MinusBlack,       // -1,234.56
135    RedParentheses,   // (1,234.56) in red
136    BlackParentheses, // (1,234.56) in black
137    MinusRed,         // -1,234.56 in red
138}
139
140/// Special format types
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum SpecialFormat {
143    ZipCode,      // 12345 or 12345-6789
144    ZipCodePlus4, // 12345-6789
145    PhoneNumber,  // (123) 456-7890
146    SSN,          // 123-45-6789
147}
148
149/// Calculation event for logging
150#[derive(Debug, Clone)]
151#[allow(dead_code)]
152pub struct CalculationEvent {
153    /// Timestamp
154    timestamp: DateTime<Utc>,
155    /// Field that triggered the event
156    field: String,
157    /// Event type
158    event_type: EventType,
159    /// Old value
160    old_value: Option<FieldValue>,
161    /// New value
162    new_value: Option<FieldValue>,
163}
164
165/// Event types
166#[derive(Debug, Clone, PartialEq)]
167pub enum EventType {
168    ValueChanged,
169    CalculationTriggered,
170    ValidationFailed,
171    FormatApplied,
172    DependencyUpdated,
173}
174
175/// Calculation system settings
176#[derive(Debug, Clone)]
177pub struct CalculationSettings {
178    /// Enable automatic recalculation
179    pub auto_recalculate: bool,
180    /// Maximum calculation depth (to prevent infinite loops)
181    pub max_depth: usize,
182    /// Enable event logging
183    pub log_events: bool,
184    /// Decimal precision
185    pub decimal_precision: usize,
186}
187
188impl Default for CalculationSettings {
189    fn default() -> Self {
190        Self {
191            auto_recalculate: true,
192            max_depth: 100,
193            log_events: true,
194            decimal_precision: 2,
195        }
196    }
197}
198
199impl Default for FormCalculationSystem {
200    fn default() -> Self {
201        Self {
202            engine: CalculationEngine::new(),
203            js_calculations: HashMap::new(),
204            field_formats: HashMap::new(),
205            events: Vec::new(),
206            settings: CalculationSettings::default(),
207        }
208    }
209}
210
211impl FormCalculationSystem {
212    /// Create a new calculation system
213    pub fn new() -> Self {
214        Self::default()
215    }
216
217    /// Create with custom settings
218    pub fn with_settings(settings: CalculationSettings) -> Self {
219        Self {
220            settings,
221            ..Self::default()
222        }
223    }
224
225    /// Set a field value and trigger calculations
226    pub fn set_field_value(
227        &mut self,
228        field_name: impl Into<String>,
229        value: FieldValue,
230    ) -> Result<(), PdfError> {
231        let field_name = field_name.into();
232
233        // Log event if enabled
234        if self.settings.log_events {
235            let old_value = self.engine.get_field_value(&field_name).cloned();
236            self.events.push(CalculationEvent {
237                timestamp: Utc::now(),
238                field: field_name.clone(),
239                event_type: EventType::ValueChanged,
240                old_value,
241                new_value: Some(value.clone()),
242            });
243        }
244
245        // Set value in engine
246        self.engine.set_field_value(field_name.clone(), value);
247
248        // Trigger JavaScript calculations if enabled
249        if self.settings.auto_recalculate {
250            self.recalculate_js_fields(&field_name)?;
251        }
252
253        Ok(())
254    }
255
256    /// Add a JavaScript calculation
257    pub fn add_js_calculation(
258        &mut self,
259        field_name: impl Into<String>,
260        calculation: JavaScriptCalculation,
261    ) -> Result<(), PdfError> {
262        let field_name = field_name.into();
263
264        // Extract dependencies
265        let dependencies = self.extract_js_dependencies(&calculation);
266
267        // Check for circular dependencies
268        if self.would_create_cycle(&field_name, &dependencies) {
269            return Err(PdfError::InvalidStructure(format!(
270                "Circular dependency detected for field '{}'",
271                field_name
272            )));
273        }
274
275        // Store calculation
276        self.js_calculations.insert(field_name.clone(), calculation);
277
278        // Perform initial calculation
279        self.calculate_js_field(&field_name)?;
280
281        Ok(())
282    }
283
284    /// Extract dependencies from JavaScript calculation
285    fn extract_js_dependencies(&self, calc: &JavaScriptCalculation) -> HashSet<String> {
286        let mut deps = HashSet::new();
287
288        match calc {
289            JavaScriptCalculation::SimpleCalculate { fields, .. } => {
290                deps.extend(fields.iter().cloned());
291            }
292            JavaScriptCalculation::PercentCalculate {
293                base_field,
294                percent_field,
295                ..
296            } => {
297                deps.insert(base_field.clone());
298                deps.insert(percent_field.clone());
299            }
300            JavaScriptCalculation::DateCalculate {
301                start_date_field,
302                days_field,
303                ..
304            } => {
305                deps.insert(start_date_field.clone());
306                if let Some(df) = days_field {
307                    deps.insert(df.clone());
308                }
309            }
310            JavaScriptCalculation::RangeCalculate { field, .. } => {
311                deps.insert(field.clone());
312            }
313            JavaScriptCalculation::NumberCalculate { field, .. } => {
314                deps.insert(field.clone());
315            }
316            JavaScriptCalculation::Custom { dependencies, .. } => {
317                deps.extend(dependencies.iter().cloned());
318            }
319        }
320
321        deps
322    }
323
324    /// Check for circular dependencies
325    fn would_create_cycle(&self, field: &str, new_deps: &HashSet<String>) -> bool {
326        for dep in new_deps {
327            if dep == field {
328                return true; // Self-reference
329            }
330
331            // Check if dep depends on field
332            if self.depends_on(dep, field) {
333                return true;
334            }
335        }
336
337        false
338    }
339
340    /// Check if field A depends on field B
341    fn depends_on(&self, field_a: &str, field_b: &str) -> bool {
342        let mut visited = HashSet::new();
343        let mut queue = VecDeque::new();
344        queue.push_back(field_a.to_string());
345
346        while let Some(current) = queue.pop_front() {
347            if current == field_b {
348                return true;
349            }
350
351            if visited.contains(&current) {
352                continue;
353            }
354            visited.insert(current.clone());
355
356            // Check JavaScript calculation dependencies
357            if let Some(calc) = self.js_calculations.get(&current) {
358                let deps = self.extract_js_dependencies(calc);
359                for dep in deps {
360                    queue.push_back(dep);
361                }
362            }
363        }
364
365        false
366    }
367
368    /// Calculate a JavaScript field
369    fn calculate_js_field(&mut self, field_name: &str) -> Result<(), PdfError> {
370        if let Some(calculation) = self.js_calculations.get(field_name).cloned() {
371            let value = self.evaluate_js_calculation(&calculation)?;
372
373            // Apply format if specified
374            let formatted_value = if let Some(format) = self.field_formats.get(field_name) {
375                self.apply_format(value, format)?
376            } else {
377                value
378            };
379
380            self.engine.set_field_value(field_name, formatted_value);
381
382            if self.settings.log_events {
383                self.events.push(CalculationEvent {
384                    timestamp: Utc::now(),
385                    field: field_name.to_string(),
386                    event_type: EventType::CalculationTriggered,
387                    old_value: None,
388                    new_value: self.engine.get_field_value(field_name).cloned(),
389                });
390            }
391        }
392
393        Ok(())
394    }
395
396    /// Evaluate a JavaScript calculation
397    fn evaluate_js_calculation(
398        &self,
399        calc: &JavaScriptCalculation,
400    ) -> Result<FieldValue, PdfError> {
401        match calc {
402            JavaScriptCalculation::SimpleCalculate { operation, fields } => {
403                let values: Vec<f64> = fields
404                    .iter()
405                    .filter_map(|f| self.engine.get_field_value(f))
406                    .map(|v| v.to_number())
407                    .collect();
408
409                if values.is_empty() {
410                    return Ok(FieldValue::Number(0.0));
411                }
412
413                let result = match operation {
414                    SimpleOperation::Sum => values.iter().sum(),
415                    SimpleOperation::Product => values.iter().product(),
416                    SimpleOperation::Average => values.iter().sum::<f64>() / values.len() as f64,
417                    SimpleOperation::Minimum => {
418                        values.iter().cloned().fold(f64::INFINITY, f64::min)
419                    }
420                    SimpleOperation::Maximum => {
421                        values.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
422                    }
423                };
424
425                Ok(FieldValue::Number(result))
426            }
427            JavaScriptCalculation::PercentCalculate {
428                base_field,
429                percent_field,
430                mode,
431            } => {
432                let base = self
433                    .engine
434                    .get_field_value(base_field)
435                    .map(|v| v.to_number())
436                    .unwrap_or(0.0);
437                let percent = self
438                    .engine
439                    .get_field_value(percent_field)
440                    .map(|v| v.to_number())
441                    .unwrap_or(0.0);
442
443                let result = match mode {
444                    PercentMode::PercentOf => base * (percent / 100.0),
445                    PercentMode::PercentageOf => {
446                        if base != 0.0 {
447                            (percent / base) * 100.0
448                        } else {
449                            0.0
450                        }
451                    }
452                    PercentMode::AddPercent => base * (1.0 + percent / 100.0),
453                    PercentMode::SubtractPercent => base * (1.0 - percent / 100.0),
454                };
455
456                Ok(FieldValue::Number(result))
457            }
458            JavaScriptCalculation::DateCalculate {
459                start_date_field,
460                days_field,
461                format: _,
462            } => {
463                // Get start date
464                let start_date_str = self
465                    .engine
466                    .get_field_value(start_date_field)
467                    .map(|v| v.to_string())
468                    .unwrap_or_default();
469
470                // Parse date (simplified - real implementation would use format)
471                if let Ok(date) = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d") {
472                    let days = if let Some(df) = days_field {
473                        self.engine
474                            .get_field_value(df)
475                            .map(|v| v.to_number() as i64)
476                            .unwrap_or(0)
477                    } else {
478                        0
479                    };
480
481                    let result_date = date + chrono::Duration::days(days);
482                    Ok(FieldValue::Text(result_date.format("%Y-%m-%d").to_string()))
483                } else {
484                    Ok(FieldValue::Text(String::new()))
485                }
486            }
487            JavaScriptCalculation::RangeCalculate { field, min, max } => {
488                let value = self
489                    .engine
490                    .get_field_value(field)
491                    .map(|v| v.to_number())
492                    .unwrap_or(0.0);
493
494                let clamped = match (min, max) {
495                    (Some(min_val), Some(max_val)) => value.clamp(*min_val, *max_val),
496                    (Some(min_val), None) => value.max(*min_val),
497                    (None, Some(max_val)) => value.min(*max_val),
498                    (None, None) => value,
499                };
500
501                Ok(FieldValue::Number(clamped))
502            }
503            JavaScriptCalculation::NumberCalculate {
504                field,
505                decimals,
506                sep_style: _,
507                currency: _,
508            } => {
509                let value = self
510                    .engine
511                    .get_field_value(field)
512                    .map(|v| v.to_number())
513                    .unwrap_or(0.0);
514
515                // Round to specified decimals
516                let factor = 10_f64.powi(*decimals as i32);
517                let rounded = (value * factor).round() / factor;
518
519                Ok(FieldValue::Number(rounded))
520            }
521            JavaScriptCalculation::Custom { script, .. } => {
522                // Very limited custom script evaluation
523                // In production, this would use a proper JavaScript engine
524                self.evaluate_custom_script(script)
525            }
526        }
527    }
528
529    /// Evaluate custom JavaScript (very limited)
530    fn evaluate_custom_script(&self, script: &str) -> Result<FieldValue, PdfError> {
531        // This is a placeholder for custom script evaluation
532        // A real implementation would need a proper sandboxed JS engine
533
534        // For now, just handle simple cases like "field1 + field2"
535        if script.contains('+') {
536            let parts: Vec<&str> = script.split('+').collect();
537            if parts.len() == 2 {
538                let field1 = parts[0].trim();
539                let field2 = parts[1].trim();
540
541                let val1 = self
542                    .engine
543                    .get_field_value(field1)
544                    .map(|v| v.to_number())
545                    .unwrap_or(0.0);
546                let val2 = self
547                    .engine
548                    .get_field_value(field2)
549                    .map(|v| v.to_number())
550                    .unwrap_or(0.0);
551
552                return Ok(FieldValue::Number(val1 + val2));
553            }
554        }
555
556        Ok(FieldValue::Empty)
557    }
558
559    /// Recalculate JavaScript fields that depend on a changed field
560    fn recalculate_js_fields(&mut self, changed_field: &str) -> Result<(), PdfError> {
561        let mut fields_to_recalc = Vec::new();
562
563        // Find fields that depend on the changed field
564        for (field_name, calc) in &self.js_calculations {
565            let deps = self.extract_js_dependencies(calc);
566            if deps.contains(changed_field) {
567                fields_to_recalc.push(field_name.clone());
568            }
569        }
570
571        // Recalculate dependent fields
572        for field in fields_to_recalc {
573            self.calculate_js_field(&field)?;
574        }
575
576        Ok(())
577    }
578
579    /// Apply format to a field value
580    fn apply_format(
581        &self,
582        value: FieldValue,
583        format: &FieldFormat,
584    ) -> Result<FieldValue, PdfError> {
585        match format {
586            FieldFormat::Number { decimals, .. } => {
587                let num = value.to_number();
588                let factor = 10_f64.powi(*decimals as i32);
589                let rounded = (num * factor).round() / factor;
590                Ok(FieldValue::Number(rounded))
591            }
592            FieldFormat::Percent { decimals } => {
593                let num = value.to_number();
594                let factor = 10_f64.powi(*decimals as i32);
595                let rounded = (num * 100.0 * factor).round() / factor;
596                Ok(FieldValue::Text(format!("{}%", rounded)))
597            }
598            _ => Ok(value),
599        }
600    }
601
602    /// Set field format
603    pub fn set_field_format(&mut self, field_name: impl Into<String>, format: FieldFormat) {
604        self.field_formats.insert(field_name.into(), format);
605    }
606
607    /// Get calculation summary
608    pub fn get_summary(&self) -> CalculationSystemSummary {
609        CalculationSystemSummary {
610            total_fields: self.engine.get_summary().total_fields,
611            js_calculations: self.js_calculations.len(),
612            formatted_fields: self.field_formats.len(),
613            events_logged: self.events.len(),
614        }
615    }
616
617    /// Get recent events
618    pub fn get_recent_events(&self, count: usize) -> Vec<&CalculationEvent> {
619        let start = self.events.len().saturating_sub(count);
620        self.events[start..].iter().collect()
621    }
622
623    /// Clear event log
624    pub fn clear_events(&mut self) {
625        self.events.clear();
626    }
627
628    /// Export to PDF dictionary
629    pub fn to_pdf_dict(&self) -> Dictionary {
630        let mut dict = Dictionary::new();
631
632        // Add calculation order
633        let calc_order: Vec<Object> = self
634            .js_calculations
635            .keys()
636            .map(|k| Object::String(k.clone()))
637            .collect();
638
639        if !calc_order.is_empty() {
640            dict.set("CO", Object::Array(calc_order));
641        }
642
643        dict
644    }
645}
646
647/// Summary of calculation system state
648#[derive(Debug, Clone)]
649pub struct CalculationSystemSummary {
650    pub total_fields: usize,
651    pub js_calculations: usize,
652    pub formatted_fields: usize,
653    pub events_logged: usize,
654}
655
656impl fmt::Display for CalculationSystemSummary {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658        write!(
659            f,
660            "Calculation System Summary:\n\
661             - Total fields: {}\n\
662             - JavaScript calculations: {}\n\
663             - Formatted fields: {}\n\
664             - Events logged: {}",
665            self.total_fields, self.js_calculations, self.formatted_fields, self.events_logged
666        )
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    #[test]
675    fn test_simple_calculate() {
676        let mut system = FormCalculationSystem::new();
677
678        // Set field values
679        system
680            .set_field_value("field1", FieldValue::Number(10.0))
681            .unwrap();
682        system
683            .set_field_value("field2", FieldValue::Number(20.0))
684            .unwrap();
685        system
686            .set_field_value("field3", FieldValue::Number(30.0))
687            .unwrap();
688
689        // Add sum calculation
690        let calc = JavaScriptCalculation::SimpleCalculate {
691            operation: SimpleOperation::Sum,
692            fields: vec![
693                "field1".to_string(),
694                "field2".to_string(),
695                "field3".to_string(),
696            ],
697        };
698
699        system.add_js_calculation("total", calc).unwrap();
700
701        // Check result
702        let total = system.engine.get_field_value("total").unwrap();
703        assert_eq!(total.to_number(), 60.0);
704    }
705
706    #[test]
707    fn test_percent_calculate() {
708        let mut system = FormCalculationSystem::new();
709
710        system
711            .set_field_value("base", FieldValue::Number(100.0))
712            .unwrap();
713        system
714            .set_field_value("percent", FieldValue::Number(15.0))
715            .unwrap();
716
717        let calc = JavaScriptCalculation::PercentCalculate {
718            base_field: "base".to_string(),
719            percent_field: "percent".to_string(),
720            mode: PercentMode::PercentOf,
721        };
722
723        system.add_js_calculation("result", calc).unwrap();
724
725        let result = system.engine.get_field_value("result").unwrap();
726        assert_eq!(result.to_number(), 15.0);
727    }
728
729    #[test]
730    fn test_range_calculate() {
731        let mut system = FormCalculationSystem::new();
732
733        system
734            .set_field_value("value", FieldValue::Number(150.0))
735            .unwrap();
736
737        let calc = JavaScriptCalculation::RangeCalculate {
738            field: "value".to_string(),
739            min: Some(0.0),
740            max: Some(100.0),
741        };
742
743        system.add_js_calculation("clamped", calc).unwrap();
744
745        let clamped = system.engine.get_field_value("clamped").unwrap();
746        assert_eq!(clamped.to_number(), 100.0);
747    }
748
749    #[test]
750    fn test_circular_dependency_detection() {
751        let mut system = FormCalculationSystem::new();
752
753        // A depends on B
754        let calc1 = JavaScriptCalculation::SimpleCalculate {
755            operation: SimpleOperation::Sum,
756            fields: vec!["fieldB".to_string()],
757        };
758        system.add_js_calculation("fieldA", calc1).unwrap();
759
760        // Try to make B depend on A (should fail)
761        let calc2 = JavaScriptCalculation::SimpleCalculate {
762            operation: SimpleOperation::Sum,
763            fields: vec!["fieldA".to_string()],
764        };
765        let result = system.add_js_calculation("fieldB", calc2);
766
767        assert!(result.is_err());
768    }
769
770    #[test]
771    fn test_event_logging() {
772        let mut system = FormCalculationSystem::new();
773
774        system
775            .set_field_value("test", FieldValue::Number(42.0))
776            .unwrap();
777
778        assert_eq!(system.events.len(), 1);
779        assert_eq!(system.events[0].event_type, EventType::ValueChanged);
780        assert_eq!(system.events[0].field, "test");
781    }
782
783    // ===== New tests for improved coverage =====
784
785    #[test]
786    fn test_default_calculation_settings() {
787        let settings = CalculationSettings::default();
788        assert!(settings.auto_recalculate);
789        assert_eq!(settings.max_depth, 100);
790        assert!(settings.log_events);
791        assert_eq!(settings.decimal_precision, 2);
792    }
793
794    #[test]
795    fn test_form_calculation_system_default() {
796        let system = FormCalculationSystem::default();
797        let summary = system.get_summary();
798        assert_eq!(summary.total_fields, 0);
799        assert_eq!(summary.js_calculations, 0);
800        assert_eq!(summary.formatted_fields, 0);
801        assert_eq!(summary.events_logged, 0);
802    }
803
804    #[test]
805    fn test_with_settings() {
806        let settings = CalculationSettings {
807            auto_recalculate: false,
808            max_depth: 50,
809            log_events: false,
810            decimal_precision: 4,
811        };
812        let system = FormCalculationSystem::with_settings(settings.clone());
813        assert!(!system.settings.auto_recalculate);
814        assert_eq!(system.settings.max_depth, 50);
815    }
816
817    #[test]
818    fn test_simple_calculate_product() {
819        let mut system = FormCalculationSystem::new();
820        system
821            .set_field_value("a", FieldValue::Number(2.0))
822            .unwrap();
823        system
824            .set_field_value("b", FieldValue::Number(3.0))
825            .unwrap();
826        system
827            .set_field_value("c", FieldValue::Number(4.0))
828            .unwrap();
829
830        let calc = JavaScriptCalculation::SimpleCalculate {
831            operation: SimpleOperation::Product,
832            fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
833        };
834        system.add_js_calculation("product", calc).unwrap();
835
836        let result = system.engine.get_field_value("product").unwrap();
837        assert_eq!(result.to_number(), 24.0);
838    }
839
840    #[test]
841    fn test_simple_calculate_average() {
842        let mut system = FormCalculationSystem::new();
843        system
844            .set_field_value("a", FieldValue::Number(10.0))
845            .unwrap();
846        system
847            .set_field_value("b", FieldValue::Number(20.0))
848            .unwrap();
849        system
850            .set_field_value("c", FieldValue::Number(30.0))
851            .unwrap();
852
853        let calc = JavaScriptCalculation::SimpleCalculate {
854            operation: SimpleOperation::Average,
855            fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
856        };
857        system.add_js_calculation("avg", calc).unwrap();
858
859        let result = system.engine.get_field_value("avg").unwrap();
860        assert_eq!(result.to_number(), 20.0);
861    }
862
863    #[test]
864    fn test_simple_calculate_minimum() {
865        let mut system = FormCalculationSystem::new();
866        system
867            .set_field_value("a", FieldValue::Number(10.0))
868            .unwrap();
869        system
870            .set_field_value("b", FieldValue::Number(5.0))
871            .unwrap();
872        system
873            .set_field_value("c", FieldValue::Number(15.0))
874            .unwrap();
875
876        let calc = JavaScriptCalculation::SimpleCalculate {
877            operation: SimpleOperation::Minimum,
878            fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
879        };
880        system.add_js_calculation("min", calc).unwrap();
881
882        let result = system.engine.get_field_value("min").unwrap();
883        assert_eq!(result.to_number(), 5.0);
884    }
885
886    #[test]
887    fn test_simple_calculate_maximum() {
888        let mut system = FormCalculationSystem::new();
889        system
890            .set_field_value("a", FieldValue::Number(10.0))
891            .unwrap();
892        system
893            .set_field_value("b", FieldValue::Number(5.0))
894            .unwrap();
895        system
896            .set_field_value("c", FieldValue::Number(15.0))
897            .unwrap();
898
899        let calc = JavaScriptCalculation::SimpleCalculate {
900            operation: SimpleOperation::Maximum,
901            fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
902        };
903        system.add_js_calculation("max", calc).unwrap();
904
905        let result = system.engine.get_field_value("max").unwrap();
906        assert_eq!(result.to_number(), 15.0);
907    }
908
909    #[test]
910    fn test_simple_calculate_empty_fields() {
911        let mut system = FormCalculationSystem::new();
912
913        let calc = JavaScriptCalculation::SimpleCalculate {
914            operation: SimpleOperation::Sum,
915            fields: vec![],
916        };
917        system.add_js_calculation("empty_sum", calc).unwrap();
918
919        let result = system.engine.get_field_value("empty_sum").unwrap();
920        assert_eq!(result.to_number(), 0.0);
921    }
922
923    #[test]
924    fn test_percent_calculate_percentage_of() {
925        let mut system = FormCalculationSystem::new();
926        system
927            .set_field_value("base", FieldValue::Number(200.0))
928            .unwrap();
929        system
930            .set_field_value("value", FieldValue::Number(50.0))
931            .unwrap();
932
933        let calc = JavaScriptCalculation::PercentCalculate {
934            base_field: "base".to_string(),
935            percent_field: "value".to_string(),
936            mode: PercentMode::PercentageOf,
937        };
938        system.add_js_calculation("percentage", calc).unwrap();
939
940        let result = system.engine.get_field_value("percentage").unwrap();
941        assert_eq!(result.to_number(), 25.0);
942    }
943
944    #[test]
945    fn test_percent_calculate_percentage_of_zero_base() {
946        let mut system = FormCalculationSystem::new();
947        system
948            .set_field_value("base", FieldValue::Number(0.0))
949            .unwrap();
950        system
951            .set_field_value("value", FieldValue::Number(50.0))
952            .unwrap();
953
954        let calc = JavaScriptCalculation::PercentCalculate {
955            base_field: "base".to_string(),
956            percent_field: "value".to_string(),
957            mode: PercentMode::PercentageOf,
958        };
959        system.add_js_calculation("percentage", calc).unwrap();
960
961        let result = system.engine.get_field_value("percentage").unwrap();
962        assert_eq!(result.to_number(), 0.0);
963    }
964
965    #[test]
966    fn test_percent_calculate_add_percent() {
967        let mut system = FormCalculationSystem::new();
968        system
969            .set_field_value("base", FieldValue::Number(100.0))
970            .unwrap();
971        system
972            .set_field_value("percent", FieldValue::Number(10.0))
973            .unwrap();
974
975        let calc = JavaScriptCalculation::PercentCalculate {
976            base_field: "base".to_string(),
977            percent_field: "percent".to_string(),
978            mode: PercentMode::AddPercent,
979        };
980        system.add_js_calculation("with_tax", calc).unwrap();
981
982        let result = system.engine.get_field_value("with_tax").unwrap();
983        assert!((result.to_number() - 110.0).abs() < 0.0001);
984    }
985
986    #[test]
987    fn test_percent_calculate_subtract_percent() {
988        let mut system = FormCalculationSystem::new();
989        system
990            .set_field_value("base", FieldValue::Number(100.0))
991            .unwrap();
992        system
993            .set_field_value("percent", FieldValue::Number(20.0))
994            .unwrap();
995
996        let calc = JavaScriptCalculation::PercentCalculate {
997            base_field: "base".to_string(),
998            percent_field: "percent".to_string(),
999            mode: PercentMode::SubtractPercent,
1000        };
1001        system.add_js_calculation("discount", calc).unwrap();
1002
1003        let result = system.engine.get_field_value("discount").unwrap();
1004        assert_eq!(result.to_number(), 80.0);
1005    }
1006
1007    #[test]
1008    fn test_range_calculate_min_only() {
1009        let mut system = FormCalculationSystem::new();
1010        system
1011            .set_field_value("value", FieldValue::Number(-10.0))
1012            .unwrap();
1013
1014        let calc = JavaScriptCalculation::RangeCalculate {
1015            field: "value".to_string(),
1016            min: Some(0.0),
1017            max: None,
1018        };
1019        system.add_js_calculation("clamped", calc).unwrap();
1020
1021        let result = system.engine.get_field_value("clamped").unwrap();
1022        assert_eq!(result.to_number(), 0.0);
1023    }
1024
1025    #[test]
1026    fn test_range_calculate_max_only() {
1027        let mut system = FormCalculationSystem::new();
1028        system
1029            .set_field_value("value", FieldValue::Number(150.0))
1030            .unwrap();
1031
1032        let calc = JavaScriptCalculation::RangeCalculate {
1033            field: "value".to_string(),
1034            min: None,
1035            max: Some(100.0),
1036        };
1037        system.add_js_calculation("clamped", calc).unwrap();
1038
1039        let result = system.engine.get_field_value("clamped").unwrap();
1040        assert_eq!(result.to_number(), 100.0);
1041    }
1042
1043    #[test]
1044    fn test_range_calculate_no_limits() {
1045        let mut system = FormCalculationSystem::new();
1046        system
1047            .set_field_value("value", FieldValue::Number(999.0))
1048            .unwrap();
1049
1050        let calc = JavaScriptCalculation::RangeCalculate {
1051            field: "value".to_string(),
1052            min: None,
1053            max: None,
1054        };
1055        system.add_js_calculation("passthrough", calc).unwrap();
1056
1057        let result = system.engine.get_field_value("passthrough").unwrap();
1058        assert_eq!(result.to_number(), 999.0);
1059    }
1060
1061    #[test]
1062    fn test_number_calculate() {
1063        let mut system = FormCalculationSystem::new();
1064        system
1065            .set_field_value("value", FieldValue::Number(123.456789))
1066            .unwrap();
1067
1068        let calc = JavaScriptCalculation::NumberCalculate {
1069            field: "value".to_string(),
1070            decimals: 2,
1071            sep_style: SeparatorStyle::CommaPeriod,
1072            currency: Some("$".to_string()),
1073        };
1074        system.add_js_calculation("formatted", calc).unwrap();
1075
1076        let result = system.engine.get_field_value("formatted").unwrap();
1077        assert!((result.to_number() - 123.46).abs() < 0.001);
1078    }
1079
1080    #[test]
1081    fn test_date_calculate() {
1082        let mut system = FormCalculationSystem::new();
1083        system
1084            .set_field_value("start_date", FieldValue::Text("2024-01-01".to_string()))
1085            .unwrap();
1086        system
1087            .set_field_value("days", FieldValue::Number(10.0))
1088            .unwrap();
1089
1090        let calc = JavaScriptCalculation::DateCalculate {
1091            start_date_field: "start_date".to_string(),
1092            days_field: Some("days".to_string()),
1093            format: "%Y-%m-%d".to_string(),
1094        };
1095        system.add_js_calculation("end_date", calc).unwrap();
1096
1097        let result = system.engine.get_field_value("end_date").unwrap();
1098        assert_eq!(result.to_string(), "2024-01-11");
1099    }
1100
1101    #[test]
1102    fn test_date_calculate_invalid_date() {
1103        let mut system = FormCalculationSystem::new();
1104        system
1105            .set_field_value("start_date", FieldValue::Text("invalid".to_string()))
1106            .unwrap();
1107
1108        let calc = JavaScriptCalculation::DateCalculate {
1109            start_date_field: "start_date".to_string(),
1110            days_field: None,
1111            format: "%Y-%m-%d".to_string(),
1112        };
1113        system.add_js_calculation("end_date", calc).unwrap();
1114
1115        let result = system.engine.get_field_value("end_date").unwrap();
1116        assert_eq!(result.to_string(), "");
1117    }
1118
1119    #[test]
1120    fn test_date_calculate_no_days_field() {
1121        let mut system = FormCalculationSystem::new();
1122        system
1123            .set_field_value("start_date", FieldValue::Text("2024-06-15".to_string()))
1124            .unwrap();
1125
1126        let calc = JavaScriptCalculation::DateCalculate {
1127            start_date_field: "start_date".to_string(),
1128            days_field: None,
1129            format: "%Y-%m-%d".to_string(),
1130        };
1131        system.add_js_calculation("end_date", calc).unwrap();
1132
1133        let result = system.engine.get_field_value("end_date").unwrap();
1134        assert_eq!(result.to_string(), "2024-06-15");
1135    }
1136
1137    #[test]
1138    fn test_custom_script_addition() {
1139        let mut system = FormCalculationSystem::new();
1140        system
1141            .set_field_value("a", FieldValue::Number(10.0))
1142            .unwrap();
1143        system
1144            .set_field_value("b", FieldValue::Number(20.0))
1145            .unwrap();
1146
1147        let calc = JavaScriptCalculation::Custom {
1148            script: "a + b".to_string(),
1149            dependencies: vec!["a".to_string(), "b".to_string()],
1150        };
1151        system.add_js_calculation("custom_result", calc).unwrap();
1152
1153        let result = system.engine.get_field_value("custom_result").unwrap();
1154        assert_eq!(result.to_number(), 30.0);
1155    }
1156
1157    #[test]
1158    fn test_custom_script_unsupported() {
1159        let mut system = FormCalculationSystem::new();
1160
1161        let calc = JavaScriptCalculation::Custom {
1162            script: "some unsupported script".to_string(),
1163            dependencies: vec![],
1164        };
1165        system.add_js_calculation("unsupported", calc).unwrap();
1166
1167        let result = system.engine.get_field_value("unsupported").unwrap();
1168        // Should return Empty for unsupported scripts
1169        assert_eq!(result.to_number(), 0.0);
1170    }
1171
1172    #[test]
1173    fn test_self_reference_detection() {
1174        let mut system = FormCalculationSystem::new();
1175
1176        let calc = JavaScriptCalculation::SimpleCalculate {
1177            operation: SimpleOperation::Sum,
1178            fields: vec!["selfField".to_string()],
1179        };
1180        let result = system.add_js_calculation("selfField", calc);
1181
1182        assert!(result.is_err());
1183    }
1184
1185    #[test]
1186    fn test_field_format_number() {
1187        let mut system = FormCalculationSystem::new();
1188
1189        system.set_field_format(
1190            "price",
1191            FieldFormat::Number {
1192                decimals: 2,
1193                separator: SeparatorStyle::CommaPeriod,
1194                negative_style: NegativeStyle::MinusBlack,
1195                currency: Some("$".to_string()),
1196            },
1197        );
1198
1199        // Set up a calculation that uses the format
1200        system
1201            .set_field_value("raw_price", FieldValue::Number(123.456))
1202            .unwrap();
1203
1204        let calc = JavaScriptCalculation::NumberCalculate {
1205            field: "raw_price".to_string(),
1206            decimals: 2,
1207            sep_style: SeparatorStyle::CommaPeriod,
1208            currency: Some("$".to_string()),
1209        };
1210        system.add_js_calculation("price", calc).unwrap();
1211
1212        let summary = system.get_summary();
1213        assert_eq!(summary.formatted_fields, 1);
1214    }
1215
1216    #[test]
1217    fn test_field_format_percent() {
1218        let mut system = FormCalculationSystem::new();
1219
1220        system.set_field_format("rate", FieldFormat::Percent { decimals: 1 });
1221
1222        let summary = system.get_summary();
1223        assert_eq!(summary.formatted_fields, 1);
1224    }
1225
1226    #[test]
1227    fn test_apply_format_number() {
1228        let system = FormCalculationSystem::new();
1229
1230        let format = FieldFormat::Number {
1231            decimals: 2,
1232            separator: SeparatorStyle::CommaPeriod,
1233            negative_style: NegativeStyle::MinusBlack,
1234            currency: None,
1235        };
1236
1237        let result = system
1238            .apply_format(FieldValue::Number(123.456789), &format)
1239            .unwrap();
1240        assert!((result.to_number() - 123.46).abs() < 0.001);
1241    }
1242
1243    #[test]
1244    fn test_apply_format_percent() {
1245        let system = FormCalculationSystem::new();
1246
1247        let format = FieldFormat::Percent { decimals: 1 };
1248
1249        let result = system
1250            .apply_format(FieldValue::Number(0.5), &format)
1251            .unwrap();
1252        assert!(result.to_string().contains("50"));
1253    }
1254
1255    #[test]
1256    fn test_get_recent_events() {
1257        let mut system = FormCalculationSystem::new();
1258
1259        for i in 0..10 {
1260            system
1261                .set_field_value(format!("field{}", i), FieldValue::Number(i as f64))
1262                .unwrap();
1263        }
1264
1265        let recent = system.get_recent_events(5);
1266        assert_eq!(recent.len(), 5);
1267    }
1268
1269    #[test]
1270    fn test_get_recent_events_more_than_available() {
1271        let mut system = FormCalculationSystem::new();
1272
1273        system
1274            .set_field_value("field1", FieldValue::Number(1.0))
1275            .unwrap();
1276        system
1277            .set_field_value("field2", FieldValue::Number(2.0))
1278            .unwrap();
1279
1280        let recent = system.get_recent_events(100);
1281        assert_eq!(recent.len(), 2);
1282    }
1283
1284    #[test]
1285    fn test_clear_events() {
1286        let mut system = FormCalculationSystem::new();
1287
1288        system
1289            .set_field_value("field1", FieldValue::Number(1.0))
1290            .unwrap();
1291        assert!(!system.events.is_empty());
1292
1293        system.clear_events();
1294        assert!(system.events.is_empty());
1295    }
1296
1297    #[test]
1298    fn test_to_pdf_dict() {
1299        let mut system = FormCalculationSystem::new();
1300
1301        let calc = JavaScriptCalculation::SimpleCalculate {
1302            operation: SimpleOperation::Sum,
1303            fields: vec!["a".to_string(), "b".to_string()],
1304        };
1305        system.add_js_calculation("total", calc).unwrap();
1306
1307        let dict = system.to_pdf_dict();
1308        assert!(dict.get("CO").is_some());
1309    }
1310
1311    #[test]
1312    fn test_to_pdf_dict_empty() {
1313        let system = FormCalculationSystem::new();
1314        let dict = system.to_pdf_dict();
1315        assert!(dict.get("CO").is_none());
1316    }
1317
1318    #[test]
1319    fn test_calculation_system_summary_display() {
1320        let summary = CalculationSystemSummary {
1321            total_fields: 10,
1322            js_calculations: 5,
1323            formatted_fields: 3,
1324            events_logged: 20,
1325        };
1326
1327        let display = format!("{}", summary);
1328        assert!(display.contains("Total fields: 10"));
1329        assert!(display.contains("JavaScript calculations: 5"));
1330        assert!(display.contains("Formatted fields: 3"));
1331        assert!(display.contains("Events logged: 20"));
1332    }
1333
1334    #[test]
1335    fn test_auto_recalculate_disabled() {
1336        let settings = CalculationSettings {
1337            auto_recalculate: false,
1338            ..Default::default()
1339        };
1340        let mut system = FormCalculationSystem::with_settings(settings);
1341
1342        system
1343            .set_field_value("a", FieldValue::Number(10.0))
1344            .unwrap();
1345        system
1346            .set_field_value("b", FieldValue::Number(20.0))
1347            .unwrap();
1348
1349        let calc = JavaScriptCalculation::SimpleCalculate {
1350            operation: SimpleOperation::Sum,
1351            fields: vec!["a".to_string(), "b".to_string()],
1352        };
1353        system.add_js_calculation("sum", calc).unwrap();
1354
1355        // Now change a field - sum should NOT auto-update since auto_recalculate is false
1356        system
1357            .set_field_value("a", FieldValue::Number(50.0))
1358            .unwrap();
1359
1360        // Manual check - the sum was calculated at add time, but not recalculated
1361        let result = system.engine.get_field_value("sum").unwrap();
1362        assert_eq!(result.to_number(), 30.0); // Still 10 + 20 from initial calculation
1363    }
1364
1365    #[test]
1366    fn test_log_events_disabled() {
1367        let settings = CalculationSettings {
1368            log_events: false,
1369            ..Default::default()
1370        };
1371        let mut system = FormCalculationSystem::with_settings(settings);
1372
1373        system
1374            .set_field_value("field1", FieldValue::Number(1.0))
1375            .unwrap();
1376        system
1377            .set_field_value("field2", FieldValue::Number(2.0))
1378            .unwrap();
1379
1380        assert!(system.events.is_empty());
1381    }
1382
1383    #[test]
1384    fn test_separator_style_variants() {
1385        assert_eq!(SeparatorStyle::CommaPeriod, SeparatorStyle::CommaPeriod);
1386        assert_eq!(SeparatorStyle::PeriodComma, SeparatorStyle::PeriodComma);
1387        assert_eq!(SeparatorStyle::SpacePeriod, SeparatorStyle::SpacePeriod);
1388        assert_eq!(
1389            SeparatorStyle::ApostrophePeriod,
1390            SeparatorStyle::ApostrophePeriod
1391        );
1392        assert_eq!(SeparatorStyle::None, SeparatorStyle::None);
1393    }
1394
1395    #[test]
1396    fn test_negative_style_variants() {
1397        assert_eq!(NegativeStyle::MinusBlack, NegativeStyle::MinusBlack);
1398        assert_eq!(NegativeStyle::RedParentheses, NegativeStyle::RedParentheses);
1399        assert_eq!(
1400            NegativeStyle::BlackParentheses,
1401            NegativeStyle::BlackParentheses
1402        );
1403        assert_eq!(NegativeStyle::MinusRed, NegativeStyle::MinusRed);
1404    }
1405
1406    #[test]
1407    fn test_special_format_variants() {
1408        assert_eq!(SpecialFormat::ZipCode, SpecialFormat::ZipCode);
1409        assert_eq!(SpecialFormat::ZipCodePlus4, SpecialFormat::ZipCodePlus4);
1410        assert_eq!(SpecialFormat::PhoneNumber, SpecialFormat::PhoneNumber);
1411        assert_eq!(SpecialFormat::SSN, SpecialFormat::SSN);
1412    }
1413
1414    #[test]
1415    fn test_simple_operation_variants() {
1416        assert_eq!(SimpleOperation::Sum, SimpleOperation::Sum);
1417        assert_eq!(SimpleOperation::Product, SimpleOperation::Product);
1418        assert_eq!(SimpleOperation::Average, SimpleOperation::Average);
1419        assert_eq!(SimpleOperation::Minimum, SimpleOperation::Minimum);
1420        assert_eq!(SimpleOperation::Maximum, SimpleOperation::Maximum);
1421    }
1422
1423    #[test]
1424    fn test_percent_mode_variants() {
1425        assert_eq!(PercentMode::PercentOf, PercentMode::PercentOf);
1426        assert_eq!(PercentMode::PercentageOf, PercentMode::PercentageOf);
1427        assert_eq!(PercentMode::AddPercent, PercentMode::AddPercent);
1428        assert_eq!(PercentMode::SubtractPercent, PercentMode::SubtractPercent);
1429    }
1430
1431    #[test]
1432    fn test_event_type_variants() {
1433        assert_eq!(EventType::ValueChanged, EventType::ValueChanged);
1434        assert_eq!(
1435            EventType::CalculationTriggered,
1436            EventType::CalculationTriggered
1437        );
1438        assert_eq!(EventType::ValidationFailed, EventType::ValidationFailed);
1439        assert_eq!(EventType::FormatApplied, EventType::FormatApplied);
1440        assert_eq!(EventType::DependencyUpdated, EventType::DependencyUpdated);
1441    }
1442
1443    #[test]
1444    fn test_recalculate_dependent_fields() {
1445        let mut system = FormCalculationSystem::new();
1446
1447        // Set up initial values
1448        system
1449            .set_field_value("base", FieldValue::Number(100.0))
1450            .unwrap();
1451
1452        // Add a calculation that depends on base
1453        let calc = JavaScriptCalculation::SimpleCalculate {
1454            operation: SimpleOperation::Sum,
1455            fields: vec!["base".to_string()],
1456        };
1457        system.add_js_calculation("derived", calc).unwrap();
1458
1459        // Verify initial calculation
1460        let initial = system.engine.get_field_value("derived").unwrap();
1461        assert_eq!(initial.to_number(), 100.0);
1462
1463        // Change base - derived should auto-update
1464        system
1465            .set_field_value("base", FieldValue::Number(200.0))
1466            .unwrap();
1467
1468        let updated = system.engine.get_field_value("derived").unwrap();
1469        assert_eq!(updated.to_number(), 200.0);
1470    }
1471
1472    #[test]
1473    fn test_field_format_date() {
1474        let mut system = FormCalculationSystem::new();
1475
1476        system.set_field_format(
1477            "date_field",
1478            FieldFormat::Date {
1479                format: "%Y-%m-%d".to_string(),
1480            },
1481        );
1482
1483        let summary = system.get_summary();
1484        assert_eq!(summary.formatted_fields, 1);
1485    }
1486
1487    #[test]
1488    fn test_field_format_time() {
1489        let mut system = FormCalculationSystem::new();
1490
1491        system.set_field_format(
1492            "time_field",
1493            FieldFormat::Time {
1494                format: "%H:%M:%S".to_string(),
1495            },
1496        );
1497
1498        let summary = system.get_summary();
1499        assert_eq!(summary.formatted_fields, 1);
1500    }
1501
1502    #[test]
1503    fn test_field_format_special() {
1504        let mut system = FormCalculationSystem::new();
1505
1506        system.set_field_format(
1507            "ssn_field",
1508            FieldFormat::Special {
1509                format_type: SpecialFormat::SSN,
1510            },
1511        );
1512
1513        let summary = system.get_summary();
1514        assert_eq!(summary.formatted_fields, 1);
1515    }
1516
1517    #[test]
1518    fn test_field_format_custom() {
1519        let mut system = FormCalculationSystem::new();
1520
1521        system.set_field_format(
1522            "custom_field",
1523            FieldFormat::Custom {
1524                format_string: "###-###".to_string(),
1525            },
1526        );
1527
1528        let summary = system.get_summary();
1529        assert_eq!(summary.formatted_fields, 1);
1530    }
1531
1532    #[test]
1533    fn test_apply_format_passthrough() {
1534        let system = FormCalculationSystem::new();
1535
1536        // Date format should pass through the value unchanged in current implementation
1537        let format = FieldFormat::Date {
1538            format: "%Y-%m-%d".to_string(),
1539        };
1540
1541        let result = system
1542            .apply_format(FieldValue::Text("2024-01-01".to_string()), &format)
1543            .unwrap();
1544        assert_eq!(result.to_string(), "2024-01-01");
1545    }
1546}