Skip to main content

datasynth_config/
validation.rs

1//! Configuration validation.
2
3use crate::schema::GeneratorConfig;
4use datasynth_core::error::{SynthError, SynthResult};
5
6/// Maximum allowed period in months (10 years).
7const MAX_PERIOD_MONTHS: u32 = 120;
8
9/// Check if a string is in valid HH:MM time format.
10fn is_valid_time_format(s: &str) -> bool {
11    if s.len() != 5 {
12        return false;
13    }
14    let chars: Vec<char> = s.chars().collect();
15    if chars[2] != ':' {
16        return false;
17    }
18    // Check hours (00-23)
19    let hours: Option<u8> = s[0..2].parse().ok();
20    let minutes: Option<u8> = s[3..5].parse().ok();
21    match (hours, minutes) {
22        (Some(h), Some(m)) => h <= 23 && m <= 59,
23        _ => false,
24    }
25}
26
27/// Validate a generator configuration.
28pub fn validate_config(config: &GeneratorConfig) -> SynthResult<()> {
29    validate_global_settings(config)?;
30    validate_companies(config)?;
31    validate_transactions(config)?;
32    validate_output(config)?;
33    validate_fraud(config)?;
34    validate_internal_controls(config)?;
35    validate_approval(config)?;
36    validate_master_data(config)?;
37    validate_document_flows(config)?;
38    validate_intercompany(config)?;
39    validate_balance(config)?;
40    validate_accounting_standards(config)?;
41    validate_audit_standards(config)?;
42    validate_distributions(config)?;
43    validate_temporal_patterns(config)?;
44    validate_vendor_network(config)?;
45    validate_customer_segmentation(config)?;
46    validate_relationship_strength(config)?;
47    validate_cross_process_links(config)?;
48    validate_anomaly_injection(config)?;
49    Ok(())
50}
51
52/// Validate global settings.
53fn validate_global_settings(config: &GeneratorConfig) -> SynthResult<()> {
54    if config.global.period_months == 0 {
55        return Err(SynthError::validation(
56            "period_months must be greater than 0",
57        ));
58    }
59    if config.global.period_months > MAX_PERIOD_MONTHS {
60        return Err(SynthError::validation(format!(
61            "period_months must be at most {} (10 years), got {}",
62            MAX_PERIOD_MONTHS, config.global.period_months
63        )));
64    }
65    Ok(())
66}
67
68/// Validate company configuration.
69fn validate_companies(config: &GeneratorConfig) -> SynthResult<()> {
70    if config.companies.is_empty() {
71        return Err(SynthError::validation(
72            "At least one company must be configured",
73        ));
74    }
75
76    for company in &config.companies {
77        if company.code.is_empty() {
78            return Err(SynthError::validation("Company code cannot be empty"));
79        }
80        if company.currency.len() != 3 {
81            return Err(SynthError::validation(format!(
82                "Invalid currency code '{}' for company '{}'",
83                company.currency, company.code
84            )));
85        }
86        if company.volume_weight < 0.0 {
87            return Err(SynthError::validation(format!(
88                "volume_weight must be non-negative for company '{}'",
89                company.code
90            )));
91        }
92    }
93    Ok(())
94}
95
96/// Validate transaction configuration.
97fn validate_transactions(config: &GeneratorConfig) -> SynthResult<()> {
98    // Validate line item distribution
99    let line_dist = &config.transactions.line_item_distribution;
100    if let Err(e) = line_dist.validate() {
101        return Err(SynthError::validation(e));
102    }
103
104    // Validate source distribution sums to ~1.0
105    let source_sum = config.transactions.source_distribution.manual
106        + config.transactions.source_distribution.automated
107        + config.transactions.source_distribution.recurring
108        + config.transactions.source_distribution.adjustment;
109    if (source_sum - 1.0).abs() > 0.01 {
110        return Err(SynthError::validation(format!(
111            "Source distribution must sum to 1.0, got {}",
112            source_sum
113        )));
114    }
115
116    // Validate business process weights
117    let bp_sum = config.business_processes.o2c_weight
118        + config.business_processes.p2p_weight
119        + config.business_processes.r2r_weight
120        + config.business_processes.h2r_weight
121        + config.business_processes.a2r_weight;
122    if (bp_sum - 1.0).abs() > 0.01 {
123        return Err(SynthError::validation(format!(
124            "Business process weights must sum to 1.0, got {}",
125            bp_sum
126        )));
127    }
128
129    // Validate Benford tolerance
130    let tolerance = config.transactions.benford.tolerance;
131    if !(0.0..=1.0).contains(&tolerance) {
132        return Err(SynthError::validation(format!(
133            "benford.tolerance must be between 0.0 and 1.0, got {}",
134            tolerance
135        )));
136    }
137
138    Ok(())
139}
140
141/// Validate output configuration.
142fn validate_output(config: &GeneratorConfig) -> SynthResult<()> {
143    let level = config.output.compression.level;
144    if config.output.compression.enabled && !(1..=9).contains(&level) {
145        return Err(SynthError::validation(format!(
146            "compression.level must be between 1 and 9, got {}",
147            level
148        )));
149    }
150
151    if config.output.batch_size == 0 {
152        return Err(SynthError::validation("batch_size must be greater than 0"));
153    }
154
155    Ok(())
156}
157
158/// Validate fraud configuration.
159fn validate_fraud(config: &GeneratorConfig) -> SynthResult<()> {
160    if !config.fraud.enabled {
161        return Ok(());
162    }
163
164    if config.fraud.fraud_rate < 0.0 || config.fraud.fraud_rate > 1.0 {
165        return Err(SynthError::validation(
166            "fraud_rate must be between 0.0 and 1.0",
167        ));
168    }
169
170    if config.fraud.clustering_factor < 0.0 {
171        return Err(SynthError::validation(
172            "clustering_factor must be non-negative",
173        ));
174    }
175
176    // Validate approval thresholds are in ascending order
177    let thresholds = &config.fraud.approval_thresholds;
178    for i in 1..thresholds.len() {
179        if thresholds[i] <= thresholds[i - 1] {
180            return Err(SynthError::validation(format!(
181                "fraud.approval_thresholds must be in strictly ascending order: {} is not greater than {}",
182                thresholds[i], thresholds[i - 1]
183            )));
184        }
185    }
186
187    // Validate fraud type distribution sums to ~1.0
188    let dist = &config.fraud.fraud_type_distribution;
189    let sum = dist.suspense_account_abuse
190        + dist.fictitious_transaction
191        + dist.revenue_manipulation
192        + dist.expense_capitalization
193        + dist.split_transaction
194        + dist.timing_anomaly
195        + dist.unauthorized_access
196        + dist.duplicate_payment;
197    if (sum - 1.0).abs() > 0.01 {
198        return Err(SynthError::validation(format!(
199            "fraud_type_distribution must sum to 1.0, got {}",
200            sum
201        )));
202    }
203
204    Ok(())
205}
206
207/// Validate internal controls configuration.
208fn validate_internal_controls(config: &GeneratorConfig) -> SynthResult<()> {
209    if !config.internal_controls.enabled {
210        return Ok(());
211    }
212
213    let exception_rate = config.internal_controls.exception_rate;
214    if !(0.0..=1.0).contains(&exception_rate) {
215        return Err(SynthError::validation(format!(
216            "exception_rate must be between 0.0 and 1.0, got {}",
217            exception_rate
218        )));
219    }
220
221    let sod_rate = config.internal_controls.sod_violation_rate;
222    if !(0.0..=1.0).contains(&sod_rate) {
223        return Err(SynthError::validation(format!(
224            "sod_violation_rate must be between 0.0 and 1.0, got {}",
225            sod_rate
226        )));
227    }
228
229    if config.internal_controls.sox_materiality_threshold < 0.0 {
230        return Err(SynthError::validation(
231            "sox_materiality_threshold must be non-negative",
232        ));
233    }
234
235    Ok(())
236}
237
238/// Validate approval configuration.
239fn validate_approval(config: &GeneratorConfig) -> SynthResult<()> {
240    if !config.approval.enabled {
241        return Ok(());
242    }
243
244    if config.approval.auto_approve_threshold < 0.0 {
245        return Err(SynthError::validation(
246            "auto_approve_threshold must be non-negative",
247        ));
248    }
249
250    let rejection_rate = config.approval.rejection_rate;
251    if !(0.0..=1.0).contains(&rejection_rate) {
252        return Err(SynthError::validation(format!(
253            "rejection_rate must be between 0.0 and 1.0, got {}",
254            rejection_rate
255        )));
256    }
257
258    let revision_rate = config.approval.revision_rate;
259    if !(0.0..=1.0).contains(&revision_rate) {
260        return Err(SynthError::validation(format!(
261            "revision_rate must be between 0.0 and 1.0, got {}",
262            revision_rate
263        )));
264    }
265
266    // rejection + revision should not exceed 1.0
267    if rejection_rate + revision_rate > 1.0 {
268        return Err(SynthError::validation(format!(
269            "rejection_rate + revision_rate must not exceed 1.0, got {}",
270            rejection_rate + revision_rate
271        )));
272    }
273
274    // Validate approval thresholds are in ascending order by amount
275    let thresholds = &config.approval.thresholds;
276    for i in 1..thresholds.len() {
277        if thresholds[i].amount <= thresholds[i - 1].amount {
278            return Err(SynthError::validation(format!(
279                "approval.thresholds must have strictly ascending amounts: {} is not greater than {}",
280                thresholds[i].amount, thresholds[i - 1].amount
281            )));
282        }
283    }
284
285    Ok(())
286}
287
288/// Validate master data configuration.
289fn validate_master_data(config: &GeneratorConfig) -> SynthResult<()> {
290    // Vendor config
291    let vendor_ic = config.master_data.vendors.intercompany_percent;
292    if !(0.0..=1.0).contains(&vendor_ic) {
293        return Err(SynthError::validation(format!(
294            "vendors.intercompany_percent must be between 0.0 and 1.0, got {}",
295            vendor_ic
296        )));
297    }
298
299    // Customer config
300    let customer_ic = config.master_data.customers.intercompany_percent;
301    if !(0.0..=1.0).contains(&customer_ic) {
302        return Err(SynthError::validation(format!(
303            "customers.intercompany_percent must be between 0.0 and 1.0, got {}",
304            customer_ic
305        )));
306    }
307
308    // Material config
309    let bom_percent = config.master_data.materials.bom_percent;
310    if !(0.0..=1.0).contains(&bom_percent) {
311        return Err(SynthError::validation(format!(
312            "materials.bom_percent must be between 0.0 and 1.0, got {}",
313            bom_percent
314        )));
315    }
316
317    // Fixed asset config
318    let fully_dep = config.master_data.fixed_assets.fully_depreciated_percent;
319    if !(0.0..=1.0).contains(&fully_dep) {
320        return Err(SynthError::validation(format!(
321            "fixed_assets.fully_depreciated_percent must be between 0.0 and 1.0, got {}",
322            fully_dep
323        )));
324    }
325
326    Ok(())
327}
328
329/// Validate document flow configuration.
330fn validate_document_flows(config: &GeneratorConfig) -> SynthResult<()> {
331    // P2P config
332    let p2p = &config.document_flows.p2p;
333    if p2p.enabled {
334        validate_rate("p2p.three_way_match_rate", p2p.three_way_match_rate)?;
335        validate_rate("p2p.partial_delivery_rate", p2p.partial_delivery_rate)?;
336        validate_rate("p2p.price_variance_rate", p2p.price_variance_rate)?;
337        validate_rate("p2p.quantity_variance_rate", p2p.quantity_variance_rate)?;
338
339        if p2p.max_price_variance_percent < 0.0 {
340            return Err(SynthError::validation(
341                "p2p.max_price_variance_percent must be non-negative",
342            ));
343        }
344
345        // P2P payment behavior config
346        validate_p2p_payment_behavior(&p2p.payment_behavior)?;
347    }
348
349    // O2C config
350    let o2c = &config.document_flows.o2c;
351    if o2c.enabled {
352        validate_rate(
353            "o2c.credit_check_failure_rate",
354            o2c.credit_check_failure_rate,
355        )?;
356        validate_rate("o2c.partial_shipment_rate", o2c.partial_shipment_rate)?;
357        validate_rate("o2c.return_rate", o2c.return_rate)?;
358        validate_rate("o2c.bad_debt_rate", o2c.bad_debt_rate)?;
359
360        // Cash discount config
361        validate_rate(
362            "o2c.cash_discount.eligible_rate",
363            o2c.cash_discount.eligible_rate,
364        )?;
365        validate_rate("o2c.cash_discount.taken_rate", o2c.cash_discount.taken_rate)?;
366        validate_rate(
367            "o2c.cash_discount.discount_percent",
368            o2c.cash_discount.discount_percent,
369        )?;
370
371        // O2C payment behavior config
372        validate_o2c_payment_behavior(&o2c.payment_behavior)?;
373    }
374
375    Ok(())
376}
377
378/// Validate P2P payment behavior configuration.
379fn validate_p2p_payment_behavior(
380    config: &crate::schema::P2PPaymentBehaviorConfig,
381) -> SynthResult<()> {
382    validate_rate(
383        "p2p.payment_behavior.late_payment_rate",
384        config.late_payment_rate,
385    )?;
386    validate_rate(
387        "p2p.payment_behavior.partial_payment_rate",
388        config.partial_payment_rate,
389    )?;
390    validate_rate(
391        "p2p.payment_behavior.payment_correction_rate",
392        config.payment_correction_rate,
393    )?;
394
395    // Validate late payment days distribution sums to ~1.0
396    let late_dist = &config.late_payment_days_distribution;
397    let late_sum = late_dist.slightly_late_1_to_7
398        + late_dist.late_8_to_14
399        + late_dist.very_late_15_to_30
400        + late_dist.severely_late_31_to_60
401        + late_dist.extremely_late_over_60;
402    if (late_sum - 1.0).abs() > 0.01 {
403        return Err(SynthError::validation(format!(
404            "p2p.payment_behavior.late_payment_days_distribution must sum to 1.0, got {}",
405            late_sum
406        )));
407    }
408
409    Ok(())
410}
411
412/// Validate O2C payment behavior configuration.
413fn validate_o2c_payment_behavior(
414    config: &crate::schema::O2CPaymentBehaviorConfig,
415) -> SynthResult<()> {
416    // Validate dunning config
417    let dunning = &config.dunning;
418    if dunning.enabled {
419        validate_rate(
420            "o2c.payment_behavior.dunning.dunning_block_rate",
421            dunning.dunning_block_rate,
422        )?;
423
424        // Validate dunning level days are in ascending order
425        if dunning.level_2_days_overdue <= dunning.level_1_days_overdue {
426            return Err(SynthError::validation(
427                "dunning.level_2_days_overdue must be greater than level_1_days_overdue",
428            ));
429        }
430        if dunning.level_3_days_overdue <= dunning.level_2_days_overdue {
431            return Err(SynthError::validation(
432                "dunning.level_3_days_overdue must be greater than level_2_days_overdue",
433            ));
434        }
435        if dunning.collection_days_overdue <= dunning.level_3_days_overdue {
436            return Err(SynthError::validation(
437                "dunning.collection_days_overdue must be greater than level_3_days_overdue",
438            ));
439        }
440
441        // Validate dunning payment rates sum to ~1.0
442        let rates = &dunning.payment_after_dunning_rates;
443        let rates_sum = rates.after_level_1
444            + rates.after_level_2
445            + rates.after_level_3
446            + rates.during_collection
447            + rates.never_pay;
448        if (rates_sum - 1.0).abs() > 0.01 {
449            return Err(SynthError::validation(format!(
450                "dunning.payment_after_dunning_rates must sum to 1.0, got {}",
451                rates_sum
452            )));
453        }
454    }
455
456    // Validate partial payments config
457    let partial = &config.partial_payments;
458    validate_rate("o2c.payment_behavior.partial_payments.rate", partial.rate)?;
459    let partial_dist = &partial.percentage_distribution;
460    let partial_sum = partial_dist.pay_25_percent
461        + partial_dist.pay_50_percent
462        + partial_dist.pay_75_percent
463        + partial_dist.pay_random_percent;
464    if (partial_sum - 1.0).abs() > 0.01 {
465        return Err(SynthError::validation(format!(
466            "partial_payments.percentage_distribution must sum to 1.0, got {}",
467            partial_sum
468        )));
469    }
470
471    // Validate short payments config
472    let short = &config.short_payments;
473    validate_rate("o2c.payment_behavior.short_payments.rate", short.rate)?;
474    validate_rate(
475        "o2c.payment_behavior.short_payments.max_short_percent",
476        short.max_short_percent,
477    )?;
478    let short_dist = &short.reason_distribution;
479    let short_sum = short_dist.pricing_dispute
480        + short_dist.quality_issue
481        + short_dist.quantity_discrepancy
482        + short_dist.unauthorized_deduction
483        + short_dist.incorrect_discount;
484    if (short_sum - 1.0).abs() > 0.01 {
485        return Err(SynthError::validation(format!(
486            "short_payments.reason_distribution must sum to 1.0, got {}",
487            short_sum
488        )));
489    }
490
491    // Validate on-account payments config
492    validate_rate(
493        "o2c.payment_behavior.on_account_payments.rate",
494        config.on_account_payments.rate,
495    )?;
496
497    // Validate payment corrections config
498    let corrections = &config.payment_corrections;
499    validate_rate(
500        "o2c.payment_behavior.payment_corrections.rate",
501        corrections.rate,
502    )?;
503    let corr_dist = &corrections.type_distribution;
504    let corr_sum = corr_dist.nsf
505        + corr_dist.chargeback
506        + corr_dist.wrong_amount
507        + corr_dist.wrong_customer
508        + corr_dist.duplicate_payment;
509    if (corr_sum - 1.0).abs() > 0.01 {
510        return Err(SynthError::validation(format!(
511            "payment_corrections.type_distribution must sum to 1.0, got {}",
512            corr_sum
513        )));
514    }
515
516    Ok(())
517}
518
519/// Validate intercompany configuration.
520fn validate_intercompany(config: &GeneratorConfig) -> SynthResult<()> {
521    if !config.intercompany.enabled {
522        return Ok(());
523    }
524
525    validate_rate(
526        "intercompany.ic_transaction_rate",
527        config.intercompany.ic_transaction_rate,
528    )?;
529
530    if config.intercompany.markup_percent < 0.0 {
531        return Err(SynthError::validation(
532            "intercompany.markup_percent must be non-negative",
533        ));
534    }
535
536    // Validate IC transaction type distribution sums to ~1.0
537    let dist = &config.intercompany.transaction_type_distribution;
538    let sum = dist.goods_sale
539        + dist.service_provided
540        + dist.loan
541        + dist.dividend
542        + dist.management_fee
543        + dist.royalty
544        + dist.cost_sharing;
545    if (sum - 1.0).abs() > 0.01 {
546        return Err(SynthError::validation(format!(
547            "intercompany.transaction_type_distribution must sum to 1.0, got {}",
548            sum
549        )));
550    }
551
552    Ok(())
553}
554
555/// Validate balance configuration.
556fn validate_balance(config: &GeneratorConfig) -> SynthResult<()> {
557    let balance = &config.balance;
558
559    if balance.target_gross_margin < 0.0 || balance.target_gross_margin > 1.0 {
560        return Err(SynthError::validation(format!(
561            "target_gross_margin must be between 0.0 and 1.0, got {}",
562            balance.target_gross_margin
563        )));
564    }
565
566    if balance.target_current_ratio < 0.0 {
567        return Err(SynthError::validation(
568            "target_current_ratio must be non-negative",
569        ));
570    }
571
572    if balance.target_debt_to_equity < 0.0 {
573        return Err(SynthError::validation(
574            "target_debt_to_equity must be non-negative",
575        ));
576    }
577
578    Ok(())
579}
580
581/// Helper to validate a rate field is between 0.0 and 1.0.
582fn validate_rate(field_name: &str, value: f64) -> SynthResult<()> {
583    if !(0.0..=1.0).contains(&value) {
584        return Err(SynthError::validation(format!(
585            "{} must be between 0.0 and 1.0, got {}",
586            field_name, value
587        )));
588    }
589    Ok(())
590}
591
592/// Validate accounting standards configuration (IFRS, US GAAP).
593fn validate_accounting_standards(config: &GeneratorConfig) -> SynthResult<()> {
594    let standards = &config.accounting_standards;
595
596    if !standards.enabled {
597        return Ok(());
598    }
599
600    // Validate revenue recognition settings
601    if standards.revenue_recognition.enabled {
602        let rev = &standards.revenue_recognition;
603
604        if rev.avg_obligations_per_contract < 1.0 {
605            return Err(SynthError::validation(
606                "avg_obligations_per_contract must be >= 1.0",
607            ));
608        }
609
610        validate_rate(
611            "revenue_recognition.variable_consideration_rate",
612            rev.variable_consideration_rate,
613        )?;
614
615        validate_rate(
616            "revenue_recognition.over_time_recognition_rate",
617            rev.over_time_recognition_rate,
618        )?;
619    }
620
621    // Validate lease accounting settings
622    if standards.leases.enabled {
623        let lease = &standards.leases;
624
625        if lease.avg_lease_term_months == 0 {
626            return Err(SynthError::validation(
627                "lease.avg_lease_term_months must be > 0",
628            ));
629        }
630
631        validate_rate("lease.finance_lease_percent", lease.finance_lease_percent)?;
632        validate_rate("lease.real_estate_percent", lease.real_estate_percent)?;
633    }
634
635    // Validate fair value settings
636    if standards.fair_value.enabled {
637        let fv = &standards.fair_value;
638
639        // Level distributions should sum to approximately 1.0
640        let level_sum = fv.level1_percent + fv.level2_percent + fv.level3_percent;
641        if (level_sum - 1.0).abs() > 0.01 {
642            return Err(SynthError::validation(format!(
643                "fair_value level percentages must sum to 1.0, got {}",
644                level_sum
645            )));
646        }
647
648        validate_rate("fair_value.level1_percent", fv.level1_percent)?;
649        validate_rate("fair_value.level2_percent", fv.level2_percent)?;
650        validate_rate("fair_value.level3_percent", fv.level3_percent)?;
651    }
652
653    // Validate impairment settings
654    if standards.impairment.enabled {
655        let imp = &standards.impairment;
656
657        validate_rate("impairment.impairment_rate", imp.impairment_rate)?;
658    }
659
660    Ok(())
661}
662
663/// Validate audit standards configuration (ISA, PCAOB, SOX).
664fn validate_audit_standards(config: &GeneratorConfig) -> SynthResult<()> {
665    let standards = &config.audit_standards;
666
667    if !standards.enabled {
668        return Ok(());
669    }
670
671    // Validate ISA compliance settings
672    if standards.isa_compliance.enabled {
673        let valid_levels = ["basic", "standard", "comprehensive"];
674        if !valid_levels.contains(&standards.isa_compliance.compliance_level.as_str()) {
675            return Err(SynthError::validation(format!(
676                "isa_compliance.compliance_level must be one of {:?}, got '{}'",
677                valid_levels, standards.isa_compliance.compliance_level
678            )));
679        }
680
681        let valid_frameworks = ["isa", "pcaob", "dual"];
682        if !valid_frameworks.contains(&standards.isa_compliance.framework.as_str()) {
683            return Err(SynthError::validation(format!(
684                "isa_compliance.framework must be one of {:?}, got '{}'",
685                valid_frameworks, standards.isa_compliance.framework
686            )));
687        }
688    }
689
690    // Validate analytical procedures settings
691    if standards.analytical_procedures.enabled {
692        let ap = &standards.analytical_procedures;
693
694        if ap.procedures_per_account == 0 {
695            return Err(SynthError::validation(
696                "analytical_procedures.procedures_per_account must be > 0",
697            ));
698        }
699
700        validate_rate(
701            "analytical_procedures.variance_probability",
702            ap.variance_probability,
703        )?;
704    }
705
706    // Validate confirmations settings
707    if standards.confirmations.enabled {
708        let conf = &standards.confirmations;
709
710        validate_rate(
711            "confirmations.positive_response_rate",
712            conf.positive_response_rate,
713        )?;
714
715        validate_rate("confirmations.exception_rate", conf.exception_rate)?;
716
717        // Positive + non-response + exception should make sense
718        let total_rate = conf.positive_response_rate + conf.exception_rate;
719        if total_rate > 1.0 {
720            return Err(SynthError::validation(
721                "confirmations: positive_response_rate + exception_rate cannot exceed 1.0",
722            ));
723        }
724    }
725
726    // Validate opinion settings
727    if standards.opinion.enabled {
728        let op = &standards.opinion;
729
730        if op.generate_kam && op.average_kam_count == 0 {
731            return Err(SynthError::validation(
732                "opinion.average_kam_count must be > 0 when generate_kam is true",
733            ));
734        }
735    }
736
737    // Validate SOX settings
738    if standards.sox.enabled {
739        let sox = &standards.sox;
740
741        if sox.materiality_threshold < 0.0 {
742            return Err(SynthError::validation(
743                "sox.materiality_threshold must be >= 0",
744            ));
745        }
746    }
747
748    // Validate PCAOB settings
749    if standards.pcaob.enabled {
750        // PCAOB requires ISA dual framework or PCAOB-only
751        if standards.isa_compliance.enabled
752            && standards.isa_compliance.framework != "pcaob"
753            && standards.isa_compliance.framework != "dual"
754        {
755            return Err(SynthError::validation(
756                "When PCAOB is enabled, ISA framework must be 'pcaob' or 'dual'",
757            ));
758        }
759    }
760
761    Ok(())
762}
763
764/// Validate advanced distribution configuration.
765fn validate_distributions(config: &GeneratorConfig) -> SynthResult<()> {
766    let dist = &config.distributions;
767
768    if !dist.enabled {
769        return Ok(());
770    }
771
772    // Validate mixture model configuration
773    validate_mixture_config(&dist.amounts)?;
774
775    // Validate correlation configuration
776    validate_correlation_config(&dist.correlations)?;
777
778    // Validate conditional distributions
779    for (i, cond) in dist.conditional.iter().enumerate() {
780        validate_conditional_config(cond, i)?;
781    }
782
783    // Validate regime changes
784    validate_regime_changes(&dist.regime_changes)?;
785
786    // Validate statistical validation settings
787    validate_statistical_validation(&dist.validation)?;
788
789    Ok(())
790}
791
792/// Validate mixture model configuration.
793fn validate_mixture_config(
794    config: &crate::schema::MixtureDistributionSchemaConfig,
795) -> SynthResult<()> {
796    if !config.enabled {
797        return Ok(());
798    }
799
800    if config.components.is_empty() {
801        return Err(SynthError::validation(
802            "distributions.amounts.components cannot be empty when enabled",
803        ));
804    }
805
806    // Validate weights sum to 1.0
807    let weight_sum: f64 = config.components.iter().map(|c| c.weight).sum();
808    if (weight_sum - 1.0).abs() > 0.01 {
809        return Err(SynthError::validation(format!(
810            "distributions.amounts.components weights must sum to 1.0, got {}",
811            weight_sum
812        )));
813    }
814
815    // Validate individual components
816    for (i, comp) in config.components.iter().enumerate() {
817        if comp.weight < 0.0 || comp.weight > 1.0 {
818            return Err(SynthError::validation(format!(
819                "distributions.amounts.components[{}].weight must be between 0.0 and 1.0, got {}",
820                i, comp.weight
821            )));
822        }
823
824        if comp.sigma <= 0.0 {
825            return Err(SynthError::validation(format!(
826                "distributions.amounts.components[{}].sigma must be positive, got {}",
827                i, comp.sigma
828            )));
829        }
830    }
831
832    // Validate min/max values
833    if config.min_value < 0.0 {
834        return Err(SynthError::validation(
835            "distributions.amounts.min_value must be non-negative",
836        ));
837    }
838
839    if let Some(max) = config.max_value {
840        if max <= config.min_value {
841            return Err(SynthError::validation(format!(
842                "distributions.amounts.max_value ({}) must be greater than min_value ({})",
843                max, config.min_value
844            )));
845        }
846    }
847
848    Ok(())
849}
850
851/// Validate correlation configuration.
852fn validate_correlation_config(config: &crate::schema::CorrelationSchemaConfig) -> SynthResult<()> {
853    if !config.enabled {
854        return Ok(());
855    }
856
857    let n = config.fields.len();
858    if n < 2 {
859        return Err(SynthError::validation(
860            "distributions.correlations.fields must have at least 2 fields",
861        ));
862    }
863
864    // Check matrix size
865    let expected_matrix_size = n * (n - 1) / 2;
866    if config.matrix.len() != expected_matrix_size {
867        return Err(SynthError::validation(format!(
868            "distributions.correlations.matrix must have {} elements for {} fields, got {}",
869            expected_matrix_size,
870            n,
871            config.matrix.len()
872        )));
873    }
874
875    // Validate correlation values are in [-1, 1]
876    for (i, &r) in config.matrix.iter().enumerate() {
877        if !(-1.0..=1.0).contains(&r) {
878            return Err(SynthError::validation(format!(
879                "distributions.correlations.matrix[{}] must be in [-1, 1], got {}",
880                i, r
881            )));
882        }
883    }
884
885    // Validate expected correlations
886    for expected in &config.expected_correlations {
887        if !(-1.0..=1.0).contains(&expected.expected_r) {
888            return Err(SynthError::validation(format!(
889                "expected_correlation for ({}, {}): expected_r must be in [-1, 1], got {}",
890                expected.field1, expected.field2, expected.expected_r
891            )));
892        }
893        if expected.tolerance <= 0.0 || expected.tolerance > 1.0 {
894            return Err(SynthError::validation(format!(
895                "expected_correlation for ({}, {}): tolerance must be in (0, 1], got {}",
896                expected.field1, expected.field2, expected.tolerance
897            )));
898        }
899    }
900
901    Ok(())
902}
903
904/// Validate conditional distribution configuration.
905fn validate_conditional_config(
906    config: &crate::schema::ConditionalDistributionSchemaConfig,
907    index: usize,
908) -> SynthResult<()> {
909    if config.output_field.is_empty() {
910        return Err(SynthError::validation(format!(
911            "distributions.conditional[{}].output_field cannot be empty",
912            index
913        )));
914    }
915
916    if config.input_field.is_empty() {
917        return Err(SynthError::validation(format!(
918            "distributions.conditional[{}].input_field cannot be empty",
919            index
920        )));
921    }
922
923    // Validate breakpoints are in ascending order
924    for i in 1..config.breakpoints.len() {
925        if config.breakpoints[i].threshold <= config.breakpoints[i - 1].threshold {
926            return Err(SynthError::validation(format!(
927                "distributions.conditional[{}].breakpoints must be in ascending order: {} is not greater than {}",
928                index, config.breakpoints[i].threshold, config.breakpoints[i - 1].threshold
929            )));
930        }
931    }
932
933    // Validate min/max constraints
934    if let (Some(min), Some(max)) = (config.min_value, config.max_value) {
935        if max <= min {
936            return Err(SynthError::validation(format!(
937                "distributions.conditional[{}].max_value ({}) must be greater than min_value ({})",
938                index, max, min
939            )));
940        }
941    }
942
943    Ok(())
944}
945
946/// Validate regime change configuration.
947fn validate_regime_changes(config: &crate::schema::RegimeChangeSchemaConfig) -> SynthResult<()> {
948    if !config.enabled {
949        return Ok(());
950    }
951
952    // Validate regime change events
953    for (i, change) in config.changes.iter().enumerate() {
954        // Validate date format (basic check)
955        if change.date.is_empty() {
956            return Err(SynthError::validation(format!(
957                "distributions.regime_changes.changes[{}].date cannot be empty",
958                i
959            )));
960        }
961
962        // Validate effects have positive multipliers
963        for (j, effect) in change.effects.iter().enumerate() {
964            if effect.multiplier < 0.0 {
965                return Err(SynthError::validation(format!(
966                    "distributions.regime_changes.changes[{}].effects[{}].multiplier must be non-negative, got {}",
967                    i, j, effect.multiplier
968                )));
969            }
970        }
971    }
972
973    // Validate economic cycle if present
974    if let Some(ref cycle) = config.economic_cycle {
975        if cycle.enabled {
976            if cycle.period_months == 0 {
977                return Err(SynthError::validation(
978                    "distributions.regime_changes.economic_cycle.period_months must be > 0",
979                ));
980            }
981
982            if cycle.amplitude < 0.0 || cycle.amplitude > 1.0 {
983                return Err(SynthError::validation(format!(
984                    "distributions.regime_changes.economic_cycle.amplitude must be in [0, 1], got {}",
985                    cycle.amplitude
986                )));
987            }
988
989            // Validate recession periods
990            for (i, recession) in cycle.recessions.iter().enumerate() {
991                if recession.duration_months == 0 {
992                    return Err(SynthError::validation(format!(
993                        "distributions.regime_changes.economic_cycle.recessions[{}].duration_months must be > 0",
994                        i
995                    )));
996                }
997
998                if recession.severity < 0.0 || recession.severity > 1.0 {
999                    return Err(SynthError::validation(format!(
1000                        "distributions.regime_changes.economic_cycle.recessions[{}].severity must be in [0, 1], got {}",
1001                        i, recession.severity
1002                    )));
1003                }
1004            }
1005        }
1006    }
1007
1008    // Validate parameter drifts
1009    for (i, drift) in config.parameter_drifts.iter().enumerate() {
1010        if drift.parameter.is_empty() {
1011            return Err(SynthError::validation(format!(
1012                "distributions.regime_changes.parameter_drifts[{}].parameter cannot be empty",
1013                i
1014            )));
1015        }
1016
1017        if let Some(end) = drift.end_period {
1018            if end <= drift.start_period {
1019                return Err(SynthError::validation(format!(
1020                    "distributions.regime_changes.parameter_drifts[{}].end_period ({}) must be > start_period ({})",
1021                    i, end, drift.start_period
1022                )));
1023            }
1024        }
1025    }
1026
1027    Ok(())
1028}
1029
1030/// Validate statistical validation configuration.
1031fn validate_statistical_validation(
1032    config: &crate::schema::StatisticalValidationSchemaConfig,
1033) -> SynthResult<()> {
1034    if !config.enabled {
1035        return Ok(());
1036    }
1037
1038    // Validate test configurations
1039    for (i, test) in config.tests.iter().enumerate() {
1040        match test {
1041            crate::schema::StatisticalTestConfig::BenfordFirstDigit {
1042                threshold_mad,
1043                warning_mad,
1044            } => {
1045                if *threshold_mad <= 0.0 {
1046                    return Err(SynthError::validation(format!(
1047                        "distributions.validation.tests[{}].threshold_mad must be positive",
1048                        i
1049                    )));
1050                }
1051                if *warning_mad <= 0.0 {
1052                    return Err(SynthError::validation(format!(
1053                        "distributions.validation.tests[{}].warning_mad must be positive",
1054                        i
1055                    )));
1056                }
1057            }
1058            crate::schema::StatisticalTestConfig::DistributionFit {
1059                ks_significance, ..
1060            } => {
1061                if *ks_significance <= 0.0 || *ks_significance >= 1.0 {
1062                    return Err(SynthError::validation(format!(
1063                        "distributions.validation.tests[{}].ks_significance must be in (0, 1), got {}",
1064                        i, ks_significance
1065                    )));
1066                }
1067            }
1068            crate::schema::StatisticalTestConfig::ChiSquared { bins, significance } => {
1069                if *bins < 2 {
1070                    return Err(SynthError::validation(format!(
1071                        "distributions.validation.tests[{}].bins must be >= 2, got {}",
1072                        i, bins
1073                    )));
1074                }
1075                if *significance <= 0.0 || *significance >= 1.0 {
1076                    return Err(SynthError::validation(format!(
1077                        "distributions.validation.tests[{}].significance must be in (0, 1), got {}",
1078                        i, significance
1079                    )));
1080                }
1081            }
1082            crate::schema::StatisticalTestConfig::AndersonDarling { significance, .. } => {
1083                if *significance <= 0.0 || *significance >= 1.0 {
1084                    return Err(SynthError::validation(format!(
1085                        "distributions.validation.tests[{}].significance must be in (0, 1), got {}",
1086                        i, significance
1087                    )));
1088                }
1089            }
1090            crate::schema::StatisticalTestConfig::CorrelationCheck {
1091                expected_correlations,
1092            } => {
1093                for expected in expected_correlations {
1094                    if !(-1.0..=1.0).contains(&expected.expected_r) {
1095                        return Err(SynthError::validation(format!(
1096                            "distributions.validation.tests[{}]: expected_r must be in [-1, 1]",
1097                            i
1098                        )));
1099                    }
1100                }
1101            }
1102        }
1103    }
1104
1105    Ok(())
1106}
1107
1108/// Validate temporal patterns configuration.
1109fn validate_temporal_patterns(config: &GeneratorConfig) -> SynthResult<()> {
1110    let temporal = &config.temporal_patterns;
1111
1112    if !temporal.enabled {
1113        return Ok(());
1114    }
1115
1116    // Validate business day configuration
1117    validate_business_day_config(&temporal.business_days)?;
1118
1119    // Validate period-end configuration
1120    validate_period_end_config(&temporal.period_end)?;
1121
1122    // Validate processing lag configuration
1123    validate_processing_lag_config(&temporal.processing_lags)?;
1124
1125    // Validate calendar regions
1126    validate_calendar_config(&temporal.calendars)?;
1127
1128    // Validate fiscal calendar configuration (P2)
1129    validate_fiscal_calendar_config(&temporal.fiscal_calendar)?;
1130
1131    // Validate intra-day patterns configuration (P2)
1132    validate_intraday_config(&temporal.intraday)?;
1133
1134    // Validate timezone configuration (P2)
1135    validate_timezone_config(&temporal.timezones)?;
1136
1137    Ok(())
1138}
1139
1140/// Validate business day configuration.
1141fn validate_business_day_config(
1142    config: &crate::schema::BusinessDaySchemaConfig,
1143) -> SynthResult<()> {
1144    if !config.enabled {
1145        return Ok(());
1146    }
1147
1148    // Validate half-day policy
1149    let valid_policies = ["full_day", "half_day", "non_business_day"];
1150    if !valid_policies.contains(&config.half_day_policy.as_str()) {
1151        return Err(SynthError::validation(format!(
1152            "temporal_patterns.business_days.half_day_policy must be one of {:?}, got '{}'",
1153            valid_policies, config.half_day_policy
1154        )));
1155    }
1156
1157    // Validate month-end convention
1158    let valid_conventions = [
1159        "modified_following",
1160        "preceding",
1161        "following",
1162        "end_of_month",
1163    ];
1164    if !valid_conventions.contains(&config.month_end_convention.as_str()) {
1165        return Err(SynthError::validation(format!(
1166            "temporal_patterns.business_days.month_end_convention must be one of {:?}, got '{}'",
1167            valid_conventions, config.month_end_convention
1168        )));
1169    }
1170
1171    // Validate settlement rules
1172    let rules = &config.settlement_rules;
1173    if rules.equity_days < 0 {
1174        return Err(SynthError::validation(
1175            "temporal_patterns.business_days.settlement_rules.equity_days must be non-negative",
1176        ));
1177    }
1178    if rules.government_bonds_days < 0 {
1179        return Err(SynthError::validation(
1180            "temporal_patterns.business_days.settlement_rules.government_bonds_days must be non-negative",
1181        ));
1182    }
1183    if rules.fx_spot_days < 0 {
1184        return Err(SynthError::validation(
1185            "temporal_patterns.business_days.settlement_rules.fx_spot_days must be non-negative",
1186        ));
1187    }
1188
1189    // Validate wire cutoff time format (HH:MM)
1190    if !rules.wire_cutoff_time.contains(':') {
1191        return Err(SynthError::validation(format!(
1192            "temporal_patterns.business_days.settlement_rules.wire_cutoff_time must be in HH:MM format, got '{}'",
1193            rules.wire_cutoff_time
1194        )));
1195    }
1196
1197    // Validate weekend days if provided
1198    if let Some(ref weekend_days) = config.weekend_days {
1199        let valid_days = [
1200            "monday",
1201            "tuesday",
1202            "wednesday",
1203            "thursday",
1204            "friday",
1205            "saturday",
1206            "sunday",
1207        ];
1208        for day in weekend_days {
1209            if !valid_days.contains(&day.to_lowercase().as_str()) {
1210                return Err(SynthError::validation(format!(
1211                    "temporal_patterns.business_days.weekend_days contains invalid day '{}', must be one of {:?}",
1212                    day, valid_days
1213                )));
1214            }
1215        }
1216    }
1217
1218    Ok(())
1219}
1220
1221/// Validate period-end configuration.
1222fn validate_period_end_config(config: &crate::schema::PeriodEndSchemaConfig) -> SynthResult<()> {
1223    // Validate model type if specified
1224    if let Some(ref model) = config.model {
1225        let valid_models = ["flat", "exponential", "extended_crunch", "daily_profile"];
1226        if !valid_models.contains(&model.as_str()) {
1227            return Err(SynthError::validation(format!(
1228                "temporal_patterns.period_end.model must be one of {:?}, got '{}'",
1229                valid_models, model
1230            )));
1231        }
1232    }
1233
1234    // Validate month-end config if present
1235    if let Some(ref month_end) = config.month_end {
1236        validate_period_end_model_config(month_end, "month_end")?;
1237    }
1238
1239    // Validate quarter-end config if present
1240    if let Some(ref quarter_end) = config.quarter_end {
1241        validate_period_end_model_config(quarter_end, "quarter_end")?;
1242    }
1243
1244    // Validate year-end config if present
1245    if let Some(ref year_end) = config.year_end {
1246        validate_period_end_model_config(year_end, "year_end")?;
1247    }
1248
1249    Ok(())
1250}
1251
1252/// Validate a period-end model configuration.
1253fn validate_period_end_model_config(
1254    config: &crate::schema::PeriodEndModelSchemaConfig,
1255    name: &str,
1256) -> SynthResult<()> {
1257    // Validate multipliers are positive
1258    if let Some(mult) = config.additional_multiplier {
1259        if mult <= 0.0 {
1260            return Err(SynthError::validation(format!(
1261                "temporal_patterns.period_end.{}.additional_multiplier must be positive, got {}",
1262                name, mult
1263            )));
1264        }
1265    }
1266
1267    if let Some(mult) = config.base_multiplier {
1268        if mult <= 0.0 {
1269            return Err(SynthError::validation(format!(
1270                "temporal_patterns.period_end.{}.base_multiplier must be positive, got {}",
1271                name, mult
1272            )));
1273        }
1274    }
1275
1276    if let Some(mult) = config.peak_multiplier {
1277        if mult <= 0.0 {
1278            return Err(SynthError::validation(format!(
1279                "temporal_patterns.period_end.{}.peak_multiplier must be positive, got {}",
1280                name, mult
1281            )));
1282        }
1283    }
1284
1285    // Validate decay_rate is in valid range (0, 1] for exponential model
1286    if let Some(rate) = config.decay_rate {
1287        if rate <= 0.0 || rate > 1.0 {
1288            return Err(SynthError::validation(format!(
1289                "temporal_patterns.period_end.{}.decay_rate must be in (0, 1], got {}",
1290                name, rate
1291            )));
1292        }
1293    }
1294
1295    // Validate start_day is negative or zero
1296    if let Some(day) = config.start_day {
1297        if day > 0 {
1298            return Err(SynthError::validation(format!(
1299                "temporal_patterns.period_end.{}.start_day must be <= 0 (days before period end), got {}",
1300                name, day
1301            )));
1302        }
1303    }
1304
1305    // Validate sustained_high_days is positive
1306    if let Some(days) = config.sustained_high_days {
1307        if days <= 0 {
1308            return Err(SynthError::validation(format!(
1309                "temporal_patterns.period_end.{}.sustained_high_days must be positive, got {}",
1310                name, days
1311            )));
1312        }
1313    }
1314
1315    Ok(())
1316}
1317
1318/// Validate processing lag configuration.
1319fn validate_processing_lag_config(
1320    config: &crate::schema::ProcessingLagSchemaConfig,
1321) -> SynthResult<()> {
1322    if !config.enabled {
1323        return Ok(());
1324    }
1325
1326    // Validate lag distributions
1327    let lag_configs = [
1328        (&config.sales_order_lag, "sales_order_lag"),
1329        (&config.purchase_order_lag, "purchase_order_lag"),
1330        (&config.goods_receipt_lag, "goods_receipt_lag"),
1331        (&config.invoice_receipt_lag, "invoice_receipt_lag"),
1332        (&config.invoice_issue_lag, "invoice_issue_lag"),
1333        (&config.payment_lag, "payment_lag"),
1334        (&config.journal_entry_lag, "journal_entry_lag"),
1335    ];
1336
1337    for (lag_opt, name) in lag_configs {
1338        if let Some(lag) = lag_opt {
1339            validate_lag_distribution(lag, name)?;
1340        }
1341    }
1342
1343    // Validate cross-day posting config
1344    if let Some(ref cross_day) = config.cross_day_posting {
1345        for (hour, prob) in &cross_day.probability_by_hour {
1346            if *hour > 23 {
1347                return Err(SynthError::validation(format!(
1348                    "temporal_patterns.processing_lags.cross_day_posting.probability_by_hour contains invalid hour {}, must be 0-23",
1349                    hour
1350                )));
1351            }
1352            if !(*prob >= 0.0 && *prob <= 1.0) {
1353                return Err(SynthError::validation(format!(
1354                    "temporal_patterns.processing_lags.cross_day_posting.probability_by_hour[{}] must be in [0, 1], got {}",
1355                    hour, prob
1356                )));
1357            }
1358        }
1359    }
1360
1361    Ok(())
1362}
1363
1364/// Validate a lag distribution configuration.
1365fn validate_lag_distribution(
1366    config: &crate::schema::LagDistributionSchemaConfig,
1367    name: &str,
1368) -> SynthResult<()> {
1369    // Sigma must be positive for log-normal
1370    if config.sigma <= 0.0 {
1371        return Err(SynthError::validation(format!(
1372            "temporal_patterns.processing_lags.{}.sigma must be positive, got {}",
1373            name, config.sigma
1374        )));
1375    }
1376
1377    // Min/max hours must be non-negative and ordered correctly
1378    if let Some(min) = config.min_hours {
1379        if min < 0.0 {
1380            return Err(SynthError::validation(format!(
1381                "temporal_patterns.processing_lags.{}.min_hours must be non-negative, got {}",
1382                name, min
1383            )));
1384        }
1385    }
1386
1387    if let Some(max) = config.max_hours {
1388        if max < 0.0 {
1389            return Err(SynthError::validation(format!(
1390                "temporal_patterns.processing_lags.{}.max_hours must be non-negative, got {}",
1391                name, max
1392            )));
1393        }
1394
1395        if let Some(min) = config.min_hours {
1396            if max < min {
1397                return Err(SynthError::validation(format!(
1398                    "temporal_patterns.processing_lags.{}.max_hours ({}) must be >= min_hours ({})",
1399                    name, max, min
1400                )));
1401            }
1402        }
1403    }
1404
1405    Ok(())
1406}
1407
1408/// Validate calendar configuration.
1409fn validate_calendar_config(config: &crate::schema::CalendarSchemaConfig) -> SynthResult<()> {
1410    // Validate region codes
1411    let valid_regions = [
1412        "US", "DE", "GB", "CN", "JP", "IN", "BR", "MX", "AU", "SG", "KR",
1413    ];
1414    for region in &config.regions {
1415        let region_upper = region.to_uppercase();
1416        if !valid_regions.contains(&region_upper.as_str()) {
1417            return Err(SynthError::validation(format!(
1418                "temporal_patterns.calendars.regions contains invalid region '{}', must be one of {:?}",
1419                region, valid_regions
1420            )));
1421        }
1422    }
1423
1424    // Validate custom holidays
1425    for (i, holiday) in config.custom_holidays.iter().enumerate() {
1426        if holiday.name.is_empty() {
1427            return Err(SynthError::validation(format!(
1428                "temporal_patterns.calendars.custom_holidays[{}].name cannot be empty",
1429                i
1430            )));
1431        }
1432
1433        if holiday.month < 1 || holiday.month > 12 {
1434            return Err(SynthError::validation(format!(
1435                "temporal_patterns.calendars.custom_holidays[{}].month must be 1-12, got {}",
1436                i, holiday.month
1437            )));
1438        }
1439
1440        if holiday.day < 1 || holiday.day > 31 {
1441            return Err(SynthError::validation(format!(
1442                "temporal_patterns.calendars.custom_holidays[{}].day must be 1-31, got {}",
1443                i, holiday.day
1444            )));
1445        }
1446
1447        if holiday.activity_multiplier < 0.0 || holiday.activity_multiplier > 1.0 {
1448            return Err(SynthError::validation(format!(
1449                "temporal_patterns.calendars.custom_holidays[{}].activity_multiplier must be in [0, 1], got {}",
1450                i, holiday.activity_multiplier
1451            )));
1452        }
1453    }
1454
1455    Ok(())
1456}
1457
1458/// Validate fiscal calendar configuration.
1459fn validate_fiscal_calendar_config(
1460    config: &crate::schema::FiscalCalendarSchemaConfig,
1461) -> SynthResult<()> {
1462    if !config.enabled {
1463        return Ok(());
1464    }
1465
1466    // Validate calendar type
1467    let valid_types = [
1468        "calendar_year",
1469        "custom",
1470        "four_four_five",
1471        "thirteen_period",
1472    ];
1473    if !valid_types.contains(&config.calendar_type.as_str()) {
1474        return Err(SynthError::validation(format!(
1475            "temporal_patterns.fiscal_calendar.calendar_type must be one of {:?}, got '{}'",
1476            valid_types, config.calendar_type
1477        )));
1478    }
1479
1480    // Validate custom year start
1481    if config.calendar_type == "custom" {
1482        if let Some(month) = config.year_start_month {
1483            if !(1..=12).contains(&month) {
1484                return Err(SynthError::validation(format!(
1485                    "temporal_patterns.fiscal_calendar.year_start_month must be 1-12, got {}",
1486                    month
1487                )));
1488            }
1489        } else {
1490            return Err(SynthError::validation(
1491                "temporal_patterns.fiscal_calendar.year_start_month is required for custom calendar type",
1492            ));
1493        }
1494
1495        if let Some(day) = config.year_start_day {
1496            if !(1..=31).contains(&day) {
1497                return Err(SynthError::validation(format!(
1498                    "temporal_patterns.fiscal_calendar.year_start_day must be 1-31, got {}",
1499                    day
1500                )));
1501            }
1502        }
1503    }
1504
1505    // Validate 4-4-5 configuration
1506    if config.calendar_type == "four_four_five" {
1507        if let Some(ref cfg) = config.four_four_five {
1508            let valid_patterns = ["four_four_five", "four_five_four", "five_four_four"];
1509            if !valid_patterns.contains(&cfg.pattern.as_str()) {
1510                return Err(SynthError::validation(format!(
1511                    "temporal_patterns.fiscal_calendar.four_four_five.pattern must be one of {:?}, got '{}'",
1512                    valid_patterns, cfg.pattern
1513                )));
1514            }
1515
1516            let valid_anchors = ["first_sunday", "last_saturday", "nearest_saturday"];
1517            if !valid_anchors.contains(&cfg.anchor_type.as_str()) {
1518                return Err(SynthError::validation(format!(
1519                    "temporal_patterns.fiscal_calendar.four_four_five.anchor_type must be one of {:?}, got '{}'",
1520                    valid_anchors, cfg.anchor_type
1521                )));
1522            }
1523
1524            if !(1..=12).contains(&cfg.anchor_month) {
1525                return Err(SynthError::validation(format!(
1526                    "temporal_patterns.fiscal_calendar.four_four_five.anchor_month must be 1-12, got {}",
1527                    cfg.anchor_month
1528                )));
1529            }
1530
1531            let valid_placements = ["q4_period3", "q1_period1"];
1532            if !valid_placements.contains(&cfg.leap_week_placement.as_str()) {
1533                return Err(SynthError::validation(format!(
1534                    "temporal_patterns.fiscal_calendar.four_four_five.leap_week_placement must be one of {:?}, got '{}'",
1535                    valid_placements, cfg.leap_week_placement
1536                )));
1537            }
1538        }
1539    }
1540
1541    Ok(())
1542}
1543
1544/// Validate intra-day patterns configuration.
1545fn validate_intraday_config(config: &crate::schema::IntraDaySchemaConfig) -> SynthResult<()> {
1546    if !config.enabled {
1547        return Ok(());
1548    }
1549
1550    // Validate each segment
1551    for (i, segment) in config.segments.iter().enumerate() {
1552        // Validate segment name is not empty
1553        if segment.name.is_empty() {
1554            return Err(SynthError::validation(format!(
1555                "temporal_patterns.intraday.segments[{}].name cannot be empty",
1556                i
1557            )));
1558        }
1559
1560        // Validate time format (HH:MM) - simple check without regex
1561        if !is_valid_time_format(&segment.start) {
1562            return Err(SynthError::validation(format!(
1563                "temporal_patterns.intraday.segments[{}].start must be in HH:MM format, got '{}'",
1564                i, segment.start
1565            )));
1566        }
1567        if !is_valid_time_format(&segment.end) {
1568            return Err(SynthError::validation(format!(
1569                "temporal_patterns.intraday.segments[{}].end must be in HH:MM format, got '{}'",
1570                i, segment.end
1571            )));
1572        }
1573
1574        // Validate multiplier is positive
1575        if segment.multiplier < 0.0 {
1576            return Err(SynthError::validation(format!(
1577                "temporal_patterns.intraday.segments[{}].multiplier must be non-negative, got {}",
1578                i, segment.multiplier
1579            )));
1580        }
1581
1582        // Validate posting type
1583        let valid_posting_types = ["human", "system", "both"];
1584        if !valid_posting_types.contains(&segment.posting_type.as_str()) {
1585            return Err(SynthError::validation(format!(
1586                "temporal_patterns.intraday.segments[{}].posting_type must be one of {:?}, got '{}'",
1587                i, valid_posting_types, segment.posting_type
1588            )));
1589        }
1590    }
1591
1592    Ok(())
1593}
1594
1595/// Validate timezone configuration.
1596fn validate_timezone_config(config: &crate::schema::TimezoneSchemaConfig) -> SynthResult<()> {
1597    if !config.enabled {
1598        return Ok(());
1599    }
1600
1601    // Validate default timezone is a valid IANA timezone name
1602    if !is_valid_iana_timezone(&config.default_timezone) {
1603        return Err(SynthError::validation(format!(
1604            "temporal_patterns.timezones.default_timezone '{}' is not a valid IANA timezone name",
1605            config.default_timezone
1606        )));
1607    }
1608
1609    // Validate consolidation timezone
1610    if !is_valid_iana_timezone(&config.consolidation_timezone) {
1611        return Err(SynthError::validation(format!(
1612            "temporal_patterns.timezones.consolidation_timezone '{}' is not a valid IANA timezone name",
1613            config.consolidation_timezone
1614        )));
1615    }
1616
1617    // Validate entity mappings
1618    for (i, mapping) in config.entity_mappings.iter().enumerate() {
1619        if mapping.pattern.is_empty() {
1620            return Err(SynthError::validation(format!(
1621                "temporal_patterns.timezones.entity_mappings[{}].pattern cannot be empty",
1622                i
1623            )));
1624        }
1625
1626        if !is_valid_iana_timezone(&mapping.timezone) {
1627            return Err(SynthError::validation(format!(
1628                "temporal_patterns.timezones.entity_mappings[{}].timezone '{}' is not a valid IANA timezone name",
1629                i, mapping.timezone
1630            )));
1631        }
1632    }
1633
1634    Ok(())
1635}
1636
1637/// Check if a string is a valid IANA timezone name.
1638/// This is a simplified check that validates common timezone patterns.
1639fn is_valid_iana_timezone(tz: &str) -> bool {
1640    // Common valid timezones - check against a set of patterns
1641    // IANA timezones are typically in format "Continent/City" or "Etc/GMT+N"
1642    if tz == "UTC" || tz == "GMT" {
1643        return true;
1644    }
1645
1646    // Check for Continent/City format
1647    let parts: Vec<&str> = tz.split('/').collect();
1648    if parts.len() >= 2 {
1649        let valid_continents = [
1650            "Africa",
1651            "America",
1652            "Antarctica",
1653            "Arctic",
1654            "Asia",
1655            "Atlantic",
1656            "Australia",
1657            "Europe",
1658            "Indian",
1659            "Pacific",
1660            "Etc",
1661        ];
1662        if valid_continents.contains(&parts[0]) {
1663            return true;
1664        }
1665    }
1666
1667    false
1668}
1669
1670/// Validate vendor network configuration.
1671fn validate_vendor_network(config: &GeneratorConfig) -> SynthResult<()> {
1672    let vn = &config.vendor_network;
1673
1674    if !vn.enabled {
1675        return Ok(());
1676    }
1677
1678    // Validate tier depth is 1-3
1679    if vn.depth == 0 || vn.depth > 3 {
1680        return Err(SynthError::validation(format!(
1681            "vendor_network.depth must be between 1 and 3, got {}",
1682            vn.depth
1683        )));
1684    }
1685
1686    // Validate tier1 count
1687    if vn.tier1.min > vn.tier1.max {
1688        return Err(SynthError::validation(format!(
1689            "vendor_network.tier1.min ({}) must be <= max ({})",
1690            vn.tier1.min, vn.tier1.max
1691        )));
1692    }
1693
1694    // Validate tier2_per_parent count
1695    if vn.tier2_per_parent.min > vn.tier2_per_parent.max {
1696        return Err(SynthError::validation(format!(
1697            "vendor_network.tier2_per_parent.min ({}) must be <= max ({})",
1698            vn.tier2_per_parent.min, vn.tier2_per_parent.max
1699        )));
1700    }
1701
1702    // Validate tier3_per_parent count
1703    if vn.tier3_per_parent.min > vn.tier3_per_parent.max {
1704        return Err(SynthError::validation(format!(
1705            "vendor_network.tier3_per_parent.min ({}) must be <= max ({})",
1706            vn.tier3_per_parent.min, vn.tier3_per_parent.max
1707        )));
1708    }
1709
1710    // Validate cluster distribution sums to ~1.0
1711    let clusters = &vn.clusters;
1712    let cluster_sum = clusters.reliable_strategic
1713        + clusters.standard_operational
1714        + clusters.transactional
1715        + clusters.problematic;
1716    if (cluster_sum - 1.0).abs() > 0.01 {
1717        return Err(SynthError::validation(format!(
1718            "vendor_network.clusters distribution must sum to 1.0, got {}",
1719            cluster_sum
1720        )));
1721    }
1722
1723    // Validate concentration limits are in valid range
1724    let deps = &vn.dependencies;
1725    validate_rate(
1726        "vendor_network.dependencies.max_single_vendor_concentration",
1727        deps.max_single_vendor_concentration,
1728    )?;
1729    validate_rate(
1730        "vendor_network.dependencies.top_5_concentration",
1731        deps.top_5_concentration,
1732    )?;
1733    validate_rate(
1734        "vendor_network.dependencies.single_source_percent",
1735        deps.single_source_percent,
1736    )?;
1737
1738    // Max single vendor should be less than top 5 (logical constraint)
1739    if deps.max_single_vendor_concentration > deps.top_5_concentration {
1740        return Err(SynthError::validation(format!(
1741            "vendor_network.dependencies.max_single_vendor_concentration ({}) should be <= top_5_concentration ({})",
1742            deps.max_single_vendor_concentration, deps.top_5_concentration
1743        )));
1744    }
1745
1746    Ok(())
1747}
1748
1749/// Validate customer segmentation configuration.
1750fn validate_customer_segmentation(config: &GeneratorConfig) -> SynthResult<()> {
1751    let cs = &config.customer_segmentation;
1752
1753    if !cs.enabled {
1754        return Ok(());
1755    }
1756
1757    // Validate value segments
1758    let segments = &cs.value_segments;
1759
1760    // Validate revenue shares sum to ~1.0
1761    let revenue_sum = segments.enterprise.revenue_share
1762        + segments.mid_market.revenue_share
1763        + segments.smb.revenue_share
1764        + segments.consumer.revenue_share;
1765    if (revenue_sum - 1.0).abs() > 0.01 {
1766        return Err(SynthError::validation(format!(
1767            "customer_segmentation.value_segments revenue_share must sum to 1.0, got {}",
1768            revenue_sum
1769        )));
1770    }
1771
1772    // Validate customer shares sum to ~1.0
1773    let customer_sum = segments.enterprise.customer_share
1774        + segments.mid_market.customer_share
1775        + segments.smb.customer_share
1776        + segments.consumer.customer_share;
1777    if (customer_sum - 1.0).abs() > 0.01 {
1778        return Err(SynthError::validation(format!(
1779            "customer_segmentation.value_segments customer_share must sum to 1.0, got {}",
1780            customer_sum
1781        )));
1782    }
1783
1784    // Validate each segment's shares are in valid range
1785    for (name, seg) in [
1786        ("enterprise", &segments.enterprise),
1787        ("mid_market", &segments.mid_market),
1788        ("smb", &segments.smb),
1789        ("consumer", &segments.consumer),
1790    ] {
1791        validate_rate(
1792            &format!(
1793                "customer_segmentation.value_segments.{}.revenue_share",
1794                name
1795            ),
1796            seg.revenue_share,
1797        )?;
1798        validate_rate(
1799            &format!(
1800                "customer_segmentation.value_segments.{}.customer_share",
1801                name
1802            ),
1803            seg.customer_share,
1804        )?;
1805    }
1806
1807    // Validate lifecycle distribution sums to ~1.0
1808    let lifecycle = &cs.lifecycle;
1809    let lifecycle_sum = lifecycle.prospect_rate
1810        + lifecycle.new_rate
1811        + lifecycle.growth_rate
1812        + lifecycle.mature_rate
1813        + lifecycle.at_risk_rate
1814        + lifecycle.churned_rate;
1815    if (lifecycle_sum - 1.0).abs() > 0.01 {
1816        return Err(SynthError::validation(format!(
1817            "customer_segmentation.lifecycle distribution must sum to 1.0, got {}",
1818            lifecycle_sum
1819        )));
1820    }
1821
1822    // Validate network config
1823    let networks = &cs.networks;
1824    validate_rate(
1825        "customer_segmentation.networks.referrals.referral_rate",
1826        networks.referrals.referral_rate,
1827    )?;
1828    validate_rate(
1829        "customer_segmentation.networks.corporate_hierarchies.probability",
1830        networks.corporate_hierarchies.probability,
1831    )?;
1832
1833    Ok(())
1834}
1835
1836/// Validate relationship strength configuration.
1837fn validate_relationship_strength(config: &GeneratorConfig) -> SynthResult<()> {
1838    let rs = &config.relationship_strength;
1839
1840    if !rs.enabled {
1841        return Ok(());
1842    }
1843
1844    // Validate calculation weights sum to ~1.0
1845    let calc = &rs.calculation;
1846    let weight_sum = calc.transaction_volume_weight
1847        + calc.transaction_count_weight
1848        + calc.relationship_duration_weight
1849        + calc.recency_weight
1850        + calc.mutual_connections_weight;
1851    if (weight_sum - 1.0).abs() > 0.01 {
1852        return Err(SynthError::validation(format!(
1853            "relationship_strength.calculation weights must sum to 1.0, got {}",
1854            weight_sum
1855        )));
1856    }
1857
1858    // Validate individual weights are in valid range
1859    validate_rate(
1860        "relationship_strength.calculation.transaction_volume_weight",
1861        calc.transaction_volume_weight,
1862    )?;
1863    validate_rate(
1864        "relationship_strength.calculation.transaction_count_weight",
1865        calc.transaction_count_weight,
1866    )?;
1867    validate_rate(
1868        "relationship_strength.calculation.relationship_duration_weight",
1869        calc.relationship_duration_weight,
1870    )?;
1871    validate_rate(
1872        "relationship_strength.calculation.recency_weight",
1873        calc.recency_weight,
1874    )?;
1875    validate_rate(
1876        "relationship_strength.calculation.mutual_connections_weight",
1877        calc.mutual_connections_weight,
1878    )?;
1879
1880    // Validate recency half-life is positive
1881    if calc.recency_half_life_days == 0 {
1882        return Err(SynthError::validation(
1883            "relationship_strength.calculation.recency_half_life_days must be positive",
1884        ));
1885    }
1886
1887    // Validate thresholds are in valid range and descending order
1888    let thresh = &rs.thresholds;
1889    validate_rate("relationship_strength.thresholds.strong", thresh.strong)?;
1890    validate_rate("relationship_strength.thresholds.moderate", thresh.moderate)?;
1891    validate_rate("relationship_strength.thresholds.weak", thresh.weak)?;
1892
1893    // Thresholds should be in descending order: strong > moderate > weak
1894    if thresh.strong <= thresh.moderate {
1895        return Err(SynthError::validation(format!(
1896            "relationship_strength.thresholds.strong ({}) must be > moderate ({})",
1897            thresh.strong, thresh.moderate
1898        )));
1899    }
1900    if thresh.moderate <= thresh.weak {
1901        return Err(SynthError::validation(format!(
1902            "relationship_strength.thresholds.moderate ({}) must be > weak ({})",
1903            thresh.moderate, thresh.weak
1904        )));
1905    }
1906
1907    Ok(())
1908}
1909
1910/// Validate cross-process links configuration.
1911fn validate_cross_process_links(config: &GeneratorConfig) -> SynthResult<()> {
1912    let cpl = &config.cross_process_links;
1913
1914    if !cpl.enabled {
1915        return Ok(());
1916    }
1917
1918    // Cross-process links are boolean flags, so there's not much to validate
1919    // beyond ensuring they're consistent with other config settings
1920
1921    // If inventory P2P-O2C links are enabled, ensure both document flows are enabled
1922    if cpl.inventory_p2p_o2c {
1923        if !config.document_flows.p2p.enabled {
1924            return Err(SynthError::validation(
1925                "cross_process_links.inventory_p2p_o2c requires document_flows.p2p to be enabled",
1926            ));
1927        }
1928        if !config.document_flows.o2c.enabled {
1929            return Err(SynthError::validation(
1930                "cross_process_links.inventory_p2p_o2c requires document_flows.o2c to be enabled",
1931            ));
1932        }
1933    }
1934
1935    // If intercompany bilateral links are enabled, ensure intercompany is enabled
1936    if cpl.intercompany_bilateral && !config.intercompany.enabled {
1937        return Err(SynthError::validation(
1938            "cross_process_links.intercompany_bilateral requires intercompany to be enabled",
1939        ));
1940    }
1941
1942    Ok(())
1943}
1944
1945/// Validate enhanced anomaly injection configuration.
1946fn validate_anomaly_injection(config: &GeneratorConfig) -> SynthResult<()> {
1947    let ai = &config.anomaly_injection;
1948
1949    if !ai.enabled {
1950        return Ok(());
1951    }
1952
1953    // Validate rates are within bounds
1954    if ai.rates.total_rate < 0.0 || ai.rates.total_rate > 1.0 {
1955        return Err(SynthError::validation(
1956            "anomaly_injection.rates.total_rate must be between 0.0 and 1.0",
1957        ));
1958    }
1959    if ai.rates.fraud_rate < 0.0 || ai.rates.fraud_rate > 1.0 {
1960        return Err(SynthError::validation(
1961            "anomaly_injection.rates.fraud_rate must be between 0.0 and 1.0",
1962        ));
1963    }
1964    if ai.rates.error_rate < 0.0 || ai.rates.error_rate > 1.0 {
1965        return Err(SynthError::validation(
1966            "anomaly_injection.rates.error_rate must be between 0.0 and 1.0",
1967        ));
1968    }
1969    if ai.rates.process_rate < 0.0 || ai.rates.process_rate > 1.0 {
1970        return Err(SynthError::validation(
1971            "anomaly_injection.rates.process_rate must be between 0.0 and 1.0",
1972        ));
1973    }
1974
1975    // Validate sub-rates don't exceed total rate
1976    let sub_rate_sum = ai.rates.fraud_rate + ai.rates.error_rate + ai.rates.process_rate;
1977    if sub_rate_sum > ai.rates.total_rate + 0.001 {
1978        return Err(SynthError::validation(format!(
1979            "anomaly_injection sub-rates sum ({}) exceeds total_rate ({})",
1980            sub_rate_sum, ai.rates.total_rate
1981        )));
1982    }
1983
1984    // Validate multi-stage scheme probabilities
1985    if ai.multi_stage_schemes.enabled {
1986        let emb = &ai.multi_stage_schemes.embezzlement;
1987        if emb.probability < 0.0 || emb.probability > 1.0 {
1988            return Err(SynthError::validation(
1989                "embezzlement.probability must be between 0.0 and 1.0",
1990            ));
1991        }
1992
1993        let rev = &ai.multi_stage_schemes.revenue_manipulation;
1994        if rev.probability < 0.0 || rev.probability > 1.0 {
1995            return Err(SynthError::validation(
1996                "revenue_manipulation.probability must be between 0.0 and 1.0",
1997            ));
1998        }
1999
2000        let kick = &ai.multi_stage_schemes.kickback;
2001        if kick.probability < 0.0 || kick.probability > 1.0 {
2002            return Err(SynthError::validation(
2003                "kickback.probability must be between 0.0 and 1.0",
2004            ));
2005        }
2006        if kick.inflation_min > kick.inflation_max {
2007            return Err(SynthError::validation(
2008                "kickback.inflation_min must be less than or equal to inflation_max",
2009            ));
2010        }
2011    }
2012
2013    // Validate near-miss configuration
2014    if ai.near_miss.enabled {
2015        if ai.near_miss.proportion < 0.0 || ai.near_miss.proportion > 1.0 {
2016            return Err(SynthError::validation(
2017                "near_miss.proportion must be between 0.0 and 1.0",
2018            ));
2019        }
2020        if ai.near_miss.near_duplicate_days.min > ai.near_miss.near_duplicate_days.max {
2021            return Err(SynthError::validation(
2022                "near_miss.near_duplicate_days.min must be less than or equal to max",
2023            ));
2024        }
2025        if ai.near_miss.threshold_proximity_range.min > ai.near_miss.threshold_proximity_range.max {
2026            return Err(SynthError::validation(
2027                "near_miss.threshold_proximity_range.min must be less than or equal to max",
2028            ));
2029        }
2030        if ai.near_miss.corrected_error_lag.min > ai.near_miss.corrected_error_lag.max {
2031            return Err(SynthError::validation(
2032                "near_miss.corrected_error_lag.min must be less than or equal to max",
2033            ));
2034        }
2035    }
2036
2037    // Validate difficulty distribution sums to ~1.0
2038    if ai.difficulty_classification.enabled {
2039        let dist = &ai.difficulty_classification.target_distribution;
2040        let dist_sum = dist.trivial + dist.easy + dist.moderate + dist.hard + dist.expert;
2041        if (dist_sum - 1.0).abs() > 0.01 {
2042            return Err(SynthError::validation(format!(
2043                "difficulty_classification.target_distribution must sum to 1.0, got {}",
2044                dist_sum
2045            )));
2046        }
2047    }
2048
2049    // Validate context-aware configuration
2050    if ai.context_aware.enabled {
2051        let vendor = &ai.context_aware.vendor_rules;
2052        if vendor.new_vendor_error_multiplier < 1.0 {
2053            return Err(SynthError::validation(
2054                "vendor_rules.new_vendor_error_multiplier must be >= 1.0",
2055            ));
2056        }
2057
2058        let emp = &ai.context_aware.employee_rules;
2059        if emp.new_employee_error_rate < 0.0 || emp.new_employee_error_rate > 1.0 {
2060            return Err(SynthError::validation(
2061                "employee_rules.new_employee_error_rate must be between 0.0 and 1.0",
2062            ));
2063        }
2064
2065        let baseline = &ai.context_aware.behavioral_baseline;
2066        if baseline.enabled && baseline.deviation_threshold_std <= 0.0 {
2067            return Err(SynthError::validation(
2068                "behavioral_baseline.deviation_threshold_std must be positive",
2069            ));
2070        }
2071    }
2072
2073    // Validate materiality thresholds are ascending
2074    let mat = &ai.labeling.materiality_thresholds;
2075    if mat.trivial >= mat.immaterial {
2076        return Err(SynthError::validation(
2077            "materiality_thresholds.trivial must be less than immaterial",
2078        ));
2079    }
2080    if mat.immaterial >= mat.material {
2081        return Err(SynthError::validation(
2082            "materiality_thresholds.immaterial must be less than material",
2083        ));
2084    }
2085    if mat.material >= mat.highly_material {
2086        return Err(SynthError::validation(
2087            "materiality_thresholds.material must be less than highly_material",
2088        ));
2089    }
2090
2091    Ok(())
2092}
2093
2094#[cfg(test)]
2095mod tests {
2096    use super::*;
2097    use crate::presets::{create_preset, demo_preset, stress_test_preset};
2098    use crate::schema::*;
2099    use datasynth_core::models::{CoAComplexity, IndustrySector};
2100
2101    /// Helper to create a minimal valid config for testing.
2102    fn minimal_valid_config() -> GeneratorConfig {
2103        GeneratorConfig {
2104            global: GlobalConfig {
2105                seed: Some(42),
2106                industry: IndustrySector::Manufacturing,
2107                start_date: "2024-01-01".to_string(),
2108                period_months: 3,
2109                group_currency: "USD".to_string(),
2110                parallel: true,
2111                worker_threads: 0,
2112                memory_limit_mb: 0,
2113            },
2114            companies: vec![CompanyConfig {
2115                code: "TEST".to_string(),
2116                name: "Test Company".to_string(),
2117                currency: "USD".to_string(),
2118                country: "US".to_string(),
2119                fiscal_year_variant: "K4".to_string(),
2120                annual_transaction_volume: TransactionVolume::TenK,
2121                volume_weight: 1.0,
2122            }],
2123            chart_of_accounts: ChartOfAccountsConfig {
2124                complexity: CoAComplexity::Small,
2125                industry_specific: true,
2126                custom_accounts: None,
2127                min_hierarchy_depth: 2,
2128                max_hierarchy_depth: 5,
2129            },
2130            transactions: TransactionConfig::default(),
2131            output: OutputConfig::default(),
2132            fraud: FraudConfig::default(),
2133            internal_controls: InternalControlsConfig::default(),
2134            business_processes: BusinessProcessConfig::default(),
2135            user_personas: UserPersonaConfig::default(),
2136            templates: TemplateConfig::default(),
2137            approval: ApprovalConfig::default(),
2138            departments: DepartmentConfig::default(),
2139            master_data: MasterDataConfig::default(),
2140            document_flows: DocumentFlowConfig::default(),
2141            intercompany: IntercompanyConfig::default(),
2142            balance: BalanceConfig::default(),
2143            ocpm: OcpmConfig::default(),
2144            audit: AuditGenerationConfig::default(),
2145            banking: datasynth_banking::BankingConfig::default(),
2146            data_quality: DataQualitySchemaConfig::default(),
2147            scenario: ScenarioConfig::default(),
2148            temporal: TemporalDriftConfig::default(),
2149            graph_export: GraphExportConfig::default(),
2150            streaming: StreamingSchemaConfig::default(),
2151            rate_limit: RateLimitSchemaConfig::default(),
2152            temporal_attributes: TemporalAttributeSchemaConfig::default(),
2153            relationships: RelationshipSchemaConfig::default(),
2154            accounting_standards: AccountingStandardsConfig::default(),
2155            audit_standards: AuditStandardsConfig::default(),
2156            distributions: AdvancedDistributionConfig::default(),
2157            temporal_patterns: TemporalPatternsConfig::default(),
2158            vendor_network: VendorNetworkSchemaConfig::default(),
2159            customer_segmentation: CustomerSegmentationSchemaConfig::default(),
2160            relationship_strength: RelationshipStrengthSchemaConfig::default(),
2161            cross_process_links: CrossProcessLinksSchemaConfig::default(),
2162            organizational_events: OrganizationalEventsSchemaConfig::default(),
2163            behavioral_drift: BehavioralDriftSchemaConfig::default(),
2164            market_drift: MarketDriftSchemaConfig::default(),
2165            drift_labeling: DriftLabelingSchemaConfig::default(),
2166            anomaly_injection: EnhancedAnomalyConfig::default(),
2167            industry_specific: IndustrySpecificConfig::default(),
2168        }
2169    }
2170
2171    // ==========================================================================
2172    // Period Months Validation Tests
2173    // ==========================================================================
2174
2175    #[test]
2176    fn test_valid_period_months() {
2177        let config = minimal_valid_config();
2178        assert!(validate_config(&config).is_ok());
2179    }
2180
2181    #[test]
2182    fn test_zero_period_months_rejected() {
2183        let mut config = minimal_valid_config();
2184        config.global.period_months = 0;
2185        let result = validate_config(&config);
2186        assert!(result.is_err());
2187        assert!(result.unwrap_err().to_string().contains("period_months"));
2188    }
2189
2190    #[test]
2191    fn test_large_period_months_accepted() {
2192        let mut config = minimal_valid_config();
2193        config.global.period_months = 120; // 10 years - maximum allowed
2194        assert!(validate_config(&config).is_ok());
2195    }
2196
2197    #[test]
2198    fn test_period_months_exceeds_max_rejected() {
2199        let mut config = minimal_valid_config();
2200        config.global.period_months = 121; // Exceeds 10 year max
2201        let result = validate_config(&config);
2202        assert!(result.is_err());
2203        assert!(result.unwrap_err().to_string().contains("period_months"));
2204    }
2205
2206    // ==========================================================================
2207    // Company Validation Tests
2208    // ==========================================================================
2209
2210    #[test]
2211    fn test_empty_companies_rejected() {
2212        let mut config = minimal_valid_config();
2213        config.companies.clear();
2214        let result = validate_config(&config);
2215        assert!(result.is_err());
2216        assert!(result.unwrap_err().to_string().contains("company"));
2217    }
2218
2219    #[test]
2220    fn test_empty_company_code_rejected() {
2221        let mut config = minimal_valid_config();
2222        config.companies[0].code = "".to_string();
2223        let result = validate_config(&config);
2224        assert!(result.is_err());
2225        assert!(result.unwrap_err().to_string().contains("Company code"));
2226    }
2227
2228    #[test]
2229    fn test_invalid_currency_code_rejected() {
2230        let mut config = minimal_valid_config();
2231        config.companies[0].currency = "US".to_string(); // 2 chars, not 3
2232        let result = validate_config(&config);
2233        assert!(result.is_err());
2234        assert!(result.unwrap_err().to_string().contains("currency"));
2235    }
2236
2237    #[test]
2238    fn test_long_currency_code_rejected() {
2239        let mut config = minimal_valid_config();
2240        config.companies[0].currency = "USDD".to_string(); // 4 chars
2241        let result = validate_config(&config);
2242        assert!(result.is_err());
2243        assert!(result.unwrap_err().to_string().contains("currency"));
2244    }
2245
2246    #[test]
2247    fn test_multiple_companies_validated() {
2248        let mut config = minimal_valid_config();
2249        config.companies.push(CompanyConfig {
2250            code: "SUB1".to_string(),
2251            name: "Subsidiary 1".to_string(),
2252            currency: "EUR".to_string(),
2253            country: "DE".to_string(),
2254            fiscal_year_variant: "K4".to_string(),
2255            annual_transaction_volume: TransactionVolume::TenK,
2256            volume_weight: 0.5,
2257        });
2258        assert!(validate_config(&config).is_ok());
2259    }
2260
2261    #[test]
2262    fn test_second_company_invalid_currency_rejected() {
2263        let mut config = minimal_valid_config();
2264        config.companies.push(CompanyConfig {
2265            code: "SUB1".to_string(),
2266            name: "Subsidiary 1".to_string(),
2267            currency: "EU".to_string(), // Invalid
2268            country: "DE".to_string(),
2269            fiscal_year_variant: "K4".to_string(),
2270            annual_transaction_volume: TransactionVolume::TenK,
2271            volume_weight: 0.5,
2272        });
2273        let result = validate_config(&config);
2274        assert!(result.is_err());
2275    }
2276
2277    // ==========================================================================
2278    // Source Distribution Validation Tests
2279    // ==========================================================================
2280
2281    #[test]
2282    fn test_valid_source_distribution() {
2283        let config = minimal_valid_config();
2284        // Default source distribution sums to 1.0
2285        assert!(validate_config(&config).is_ok());
2286    }
2287
2288    #[test]
2289    fn test_source_distribution_not_summing_to_one_rejected() {
2290        let mut config = minimal_valid_config();
2291        config.transactions.source_distribution = SourceDistribution {
2292            manual: 0.5,
2293            automated: 0.5,
2294            recurring: 0.5, // Sum = 1.6
2295            adjustment: 0.1,
2296        };
2297        let result = validate_config(&config);
2298        assert!(result.is_err());
2299        assert!(result
2300            .unwrap_err()
2301            .to_string()
2302            .contains("Source distribution"));
2303    }
2304
2305    #[test]
2306    fn test_source_distribution_slightly_off_accepted() {
2307        let mut config = minimal_valid_config();
2308        // Within 0.01 tolerance
2309        config.transactions.source_distribution = SourceDistribution {
2310            manual: 0.20,
2311            automated: 0.70,
2312            recurring: 0.07,
2313            adjustment: 0.025, // Sum = 0.995, within tolerance
2314        };
2315        assert!(validate_config(&config).is_ok());
2316    }
2317
2318    // ==========================================================================
2319    // Business Process Weights Validation Tests
2320    // ==========================================================================
2321
2322    #[test]
2323    fn test_valid_business_process_weights() {
2324        let config = minimal_valid_config();
2325        // Default weights sum to 1.0
2326        assert!(validate_config(&config).is_ok());
2327    }
2328
2329    #[test]
2330    fn test_business_process_weights_not_summing_to_one_rejected() {
2331        let mut config = minimal_valid_config();
2332        config.business_processes = BusinessProcessConfig {
2333            o2c_weight: 0.5,
2334            p2p_weight: 0.5,
2335            r2r_weight: 0.5, // Sum > 1
2336            h2r_weight: 0.1,
2337            a2r_weight: 0.1,
2338        };
2339        let result = validate_config(&config);
2340        assert!(result.is_err());
2341        assert!(result
2342            .unwrap_err()
2343            .to_string()
2344            .contains("Business process weights"));
2345    }
2346
2347    // ==========================================================================
2348    // Fraud Configuration Validation Tests
2349    // ==========================================================================
2350
2351    #[test]
2352    fn test_fraud_disabled_invalid_rate_accepted() {
2353        let mut config = minimal_valid_config();
2354        config.fraud.enabled = false;
2355        config.fraud.fraud_rate = 5.0; // Invalid but ignored since disabled
2356        assert!(validate_config(&config).is_ok());
2357    }
2358
2359    #[test]
2360    fn test_fraud_enabled_valid_rate_accepted() {
2361        let mut config = minimal_valid_config();
2362        config.fraud.enabled = true;
2363        config.fraud.fraud_rate = 0.05;
2364        assert!(validate_config(&config).is_ok());
2365    }
2366
2367    #[test]
2368    fn test_fraud_enabled_negative_rate_rejected() {
2369        let mut config = minimal_valid_config();
2370        config.fraud.enabled = true;
2371        config.fraud.fraud_rate = -0.1;
2372        let result = validate_config(&config);
2373        assert!(result.is_err());
2374        assert!(result.unwrap_err().to_string().contains("fraud_rate"));
2375    }
2376
2377    #[test]
2378    fn test_fraud_enabled_rate_above_one_rejected() {
2379        let mut config = minimal_valid_config();
2380        config.fraud.enabled = true;
2381        config.fraud.fraud_rate = 1.5;
2382        let result = validate_config(&config);
2383        assert!(result.is_err());
2384        assert!(result.unwrap_err().to_string().contains("fraud_rate"));
2385    }
2386
2387    #[test]
2388    fn test_fraud_rate_zero_accepted() {
2389        let mut config = minimal_valid_config();
2390        config.fraud.enabled = true;
2391        config.fraud.fraud_rate = 0.0;
2392        assert!(validate_config(&config).is_ok());
2393    }
2394
2395    #[test]
2396    fn test_fraud_rate_one_accepted() {
2397        let mut config = minimal_valid_config();
2398        config.fraud.enabled = true;
2399        config.fraud.fraud_rate = 1.0;
2400        assert!(validate_config(&config).is_ok());
2401    }
2402
2403    // ==========================================================================
2404    // Preset Validation Tests
2405    // ==========================================================================
2406
2407    #[test]
2408    fn test_demo_preset_valid() {
2409        let config = demo_preset();
2410        assert!(validate_config(&config).is_ok());
2411    }
2412
2413    #[test]
2414    fn test_stress_test_preset_valid() {
2415        let config = stress_test_preset();
2416        assert!(validate_config(&config).is_ok());
2417    }
2418
2419    #[test]
2420    fn test_manufacturing_preset_valid() {
2421        let config = create_preset(
2422            IndustrySector::Manufacturing,
2423            2,
2424            12,
2425            CoAComplexity::Medium,
2426            TransactionVolume::HundredK,
2427        );
2428        assert!(validate_config(&config).is_ok());
2429    }
2430
2431    #[test]
2432    fn test_retail_preset_valid() {
2433        let config = create_preset(
2434            IndustrySector::Retail,
2435            3,
2436            6,
2437            CoAComplexity::Large,
2438            TransactionVolume::OneM,
2439        );
2440        assert!(validate_config(&config).is_ok());
2441    }
2442
2443    #[test]
2444    fn test_financial_services_preset_valid() {
2445        let config = create_preset(
2446            IndustrySector::FinancialServices,
2447            2,
2448            12,
2449            CoAComplexity::Large,
2450            TransactionVolume::TenM,
2451        );
2452        assert!(validate_config(&config).is_ok());
2453    }
2454
2455    #[test]
2456    fn test_healthcare_preset_valid() {
2457        let config = create_preset(
2458            IndustrySector::Healthcare,
2459            2,
2460            6,
2461            CoAComplexity::Medium,
2462            TransactionVolume::HundredK,
2463        );
2464        assert!(validate_config(&config).is_ok());
2465    }
2466
2467    #[test]
2468    fn test_technology_preset_valid() {
2469        let config = create_preset(
2470            IndustrySector::Technology,
2471            3,
2472            12,
2473            CoAComplexity::Medium,
2474            TransactionVolume::HundredK,
2475        );
2476        assert!(validate_config(&config).is_ok());
2477    }
2478
2479    // ==========================================================================
2480    // Transaction Volume Tests
2481    // ==========================================================================
2482
2483    #[test]
2484    fn test_transaction_volume_counts() {
2485        assert_eq!(TransactionVolume::TenK.count(), 10_000);
2486        assert_eq!(TransactionVolume::HundredK.count(), 100_000);
2487        assert_eq!(TransactionVolume::OneM.count(), 1_000_000);
2488        assert_eq!(TransactionVolume::TenM.count(), 10_000_000);
2489        assert_eq!(TransactionVolume::HundredM.count(), 100_000_000);
2490        assert_eq!(TransactionVolume::Custom(50_000).count(), 50_000);
2491    }
2492
2493    // ==========================================================================
2494    // Default Value Tests
2495    // ==========================================================================
2496
2497    #[test]
2498    fn test_source_distribution_default_sums_to_one() {
2499        let dist = SourceDistribution::default();
2500        let sum = dist.manual + dist.automated + dist.recurring + dist.adjustment;
2501        assert!((sum - 1.0).abs() < 0.001);
2502    }
2503
2504    #[test]
2505    fn test_business_process_default_sums_to_one() {
2506        let bp = BusinessProcessConfig::default();
2507        let sum = bp.o2c_weight + bp.p2p_weight + bp.r2r_weight + bp.h2r_weight + bp.a2r_weight;
2508        assert!((sum - 1.0).abs() < 0.001);
2509    }
2510
2511    #[test]
2512    fn test_fraud_type_distribution_default_sums_to_one() {
2513        let dist = FraudTypeDistribution::default();
2514        let sum = dist.suspense_account_abuse
2515            + dist.fictitious_transaction
2516            + dist.revenue_manipulation
2517            + dist.expense_capitalization
2518            + dist.split_transaction
2519            + dist.timing_anomaly
2520            + dist.unauthorized_access
2521            + dist.duplicate_payment;
2522        assert!((sum - 1.0).abs() < 0.001);
2523    }
2524
2525    #[test]
2526    fn test_persona_distribution_default_sums_to_one() {
2527        let dist = PersonaDistribution::default();
2528        let sum = dist.junior_accountant
2529            + dist.senior_accountant
2530            + dist.controller
2531            + dist.manager
2532            + dist.automated_system;
2533        assert!((sum - 1.0).abs() < 0.001);
2534    }
2535
2536    #[test]
2537    fn test_payment_terms_distribution_default_sums_to_one() {
2538        let dist = PaymentTermsDistribution::default();
2539        let sum = dist.net_30
2540            + dist.net_60
2541            + dist.net_90
2542            + dist.two_ten_net_30
2543            + dist.due_on_receipt
2544            + dist.end_of_month;
2545        assert!((sum - 1.0).abs() < 0.001);
2546    }
2547
2548    #[test]
2549    fn test_vendor_behavior_distribution_default_sums_to_one() {
2550        let dist = VendorBehaviorDistribution::default();
2551        let sum = dist.reliable
2552            + dist.sometimes_late
2553            + dist.inconsistent_quality
2554            + dist.premium
2555            + dist.budget;
2556        assert!((sum - 1.0).abs() < 0.001);
2557    }
2558
2559    #[test]
2560    fn test_credit_rating_distribution_default_sums_to_one() {
2561        let dist = CreditRatingDistribution::default();
2562        let sum = dist.aaa + dist.aa + dist.a + dist.bbb + dist.bb + dist.b + dist.below_b;
2563        assert!((sum - 1.0).abs() < 0.001);
2564    }
2565
2566    #[test]
2567    fn test_payment_behavior_distribution_default_sums_to_one() {
2568        let dist = PaymentBehaviorDistribution::default();
2569        let sum = dist.early_payer
2570            + dist.on_time
2571            + dist.occasional_late
2572            + dist.frequent_late
2573            + dist.discount_taker;
2574        assert!((sum - 1.0).abs() < 0.001);
2575    }
2576
2577    #[test]
2578    fn test_material_type_distribution_default_sums_to_one() {
2579        let dist = MaterialTypeDistribution::default();
2580        let sum = dist.raw_material
2581            + dist.semi_finished
2582            + dist.finished_good
2583            + dist.trading_good
2584            + dist.operating_supply
2585            + dist.service;
2586        assert!((sum - 1.0).abs() < 0.001);
2587    }
2588
2589    #[test]
2590    fn test_valuation_method_distribution_default_sums_to_one() {
2591        let dist = ValuationMethodDistribution::default();
2592        let sum = dist.standard_cost + dist.moving_average + dist.fifo + dist.lifo;
2593        assert!((sum - 1.0).abs() < 0.001);
2594    }
2595
2596    #[test]
2597    fn test_asset_class_distribution_default_sums_to_one() {
2598        let dist = AssetClassDistribution::default();
2599        let sum = dist.buildings
2600            + dist.machinery
2601            + dist.vehicles
2602            + dist.it_equipment
2603            + dist.furniture
2604            + dist.land
2605            + dist.leasehold;
2606        assert!((sum - 1.0).abs() < 0.001);
2607    }
2608
2609    #[test]
2610    fn test_depreciation_method_distribution_default_sums_to_one() {
2611        let dist = DepreciationMethodDistribution::default();
2612        let sum = dist.straight_line
2613            + dist.declining_balance
2614            + dist.double_declining
2615            + dist.sum_of_years
2616            + dist.units_of_production;
2617        assert!((sum - 1.0).abs() < 0.001);
2618    }
2619
2620    #[test]
2621    fn test_employee_department_distribution_default_sums_to_one() {
2622        let dist = EmployeeDepartmentDistribution::default();
2623        let sum = dist.finance
2624            + dist.procurement
2625            + dist.sales
2626            + dist.warehouse
2627            + dist.it
2628            + dist.hr
2629            + dist.operations
2630            + dist.executive;
2631        assert!((sum - 1.0).abs() < 0.001);
2632    }
2633
2634    #[test]
2635    fn test_ic_transaction_type_distribution_default_sums_to_one() {
2636        let dist = ICTransactionTypeDistribution::default();
2637        let sum = dist.goods_sale
2638            + dist.service_provided
2639            + dist.loan
2640            + dist.dividend
2641            + dist.management_fee
2642            + dist.royalty
2643            + dist.cost_sharing;
2644        assert!((sum - 1.0).abs() < 0.001);
2645    }
2646
2647    // ==========================================================================
2648    // Compression Level Validation Tests
2649    // ==========================================================================
2650
2651    #[test]
2652    fn test_compression_level_valid() {
2653        let mut config = minimal_valid_config();
2654        config.output.compression.enabled = true;
2655        config.output.compression.level = 5;
2656        assert!(validate_config(&config).is_ok());
2657    }
2658
2659    #[test]
2660    fn test_compression_level_zero_rejected() {
2661        let mut config = minimal_valid_config();
2662        config.output.compression.enabled = true;
2663        config.output.compression.level = 0;
2664        let result = validate_config(&config);
2665        assert!(result.is_err());
2666        assert!(result
2667            .unwrap_err()
2668            .to_string()
2669            .contains("compression.level"));
2670    }
2671
2672    #[test]
2673    fn test_compression_level_ten_rejected() {
2674        let mut config = minimal_valid_config();
2675        config.output.compression.enabled = true;
2676        config.output.compression.level = 10;
2677        let result = validate_config(&config);
2678        assert!(result.is_err());
2679        assert!(result
2680            .unwrap_err()
2681            .to_string()
2682            .contains("compression.level"));
2683    }
2684
2685    #[test]
2686    fn test_compression_disabled_ignores_level() {
2687        let mut config = minimal_valid_config();
2688        config.output.compression.enabled = false;
2689        config.output.compression.level = 0; // Invalid but ignored
2690        assert!(validate_config(&config).is_ok());
2691    }
2692
2693    // ==========================================================================
2694    // Approval Threshold Ordering Tests
2695    // ==========================================================================
2696
2697    #[test]
2698    fn test_approval_thresholds_ascending_accepted() {
2699        let mut config = minimal_valid_config();
2700        config.approval.enabled = true;
2701        config.approval.thresholds = vec![
2702            ApprovalThresholdConfig {
2703                amount: 1000.0,
2704                level: 1,
2705                roles: vec!["accountant".to_string()],
2706            },
2707            ApprovalThresholdConfig {
2708                amount: 5000.0,
2709                level: 2,
2710                roles: vec!["manager".to_string()],
2711            },
2712            ApprovalThresholdConfig {
2713                amount: 10000.0,
2714                level: 3,
2715                roles: vec!["director".to_string()],
2716            },
2717        ];
2718        assert!(validate_config(&config).is_ok());
2719    }
2720
2721    #[test]
2722    fn test_approval_thresholds_not_ascending_rejected() {
2723        let mut config = minimal_valid_config();
2724        config.approval.enabled = true;
2725        config.approval.thresholds = vec![
2726            ApprovalThresholdConfig {
2727                amount: 5000.0,
2728                level: 1,
2729                roles: vec!["accountant".to_string()],
2730            },
2731            ApprovalThresholdConfig {
2732                amount: 1000.0, // Less than previous - invalid
2733                level: 2,
2734                roles: vec!["manager".to_string()],
2735            },
2736        ];
2737        let result = validate_config(&config);
2738        assert!(result.is_err());
2739        assert!(result.unwrap_err().to_string().contains("ascending"));
2740    }
2741
2742    #[test]
2743    fn test_fraud_approval_thresholds_ascending_accepted() {
2744        let mut config = minimal_valid_config();
2745        config.fraud.enabled = true;
2746        config.fraud.fraud_rate = 0.05;
2747        config.fraud.approval_thresholds = vec![1000.0, 5000.0, 10000.0];
2748        assert!(validate_config(&config).is_ok());
2749    }
2750
2751    #[test]
2752    fn test_fraud_approval_thresholds_not_ascending_rejected() {
2753        let mut config = minimal_valid_config();
2754        config.fraud.enabled = true;
2755        config.fraud.fraud_rate = 0.05;
2756        config.fraud.approval_thresholds = vec![5000.0, 1000.0, 10000.0]; // Not ascending
2757        let result = validate_config(&config);
2758        assert!(result.is_err());
2759        assert!(result.unwrap_err().to_string().contains("ascending"));
2760    }
2761
2762    // ==========================================================================
2763    // Rate/Percentage Validation Tests
2764    // ==========================================================================
2765
2766    #[test]
2767    fn test_internal_controls_rates_valid() {
2768        let mut config = minimal_valid_config();
2769        config.internal_controls.enabled = true;
2770        config.internal_controls.exception_rate = 0.05;
2771        config.internal_controls.sod_violation_rate = 0.02;
2772        assert!(validate_config(&config).is_ok());
2773    }
2774
2775    #[test]
2776    fn test_internal_controls_exception_rate_invalid() {
2777        let mut config = minimal_valid_config();
2778        config.internal_controls.enabled = true;
2779        config.internal_controls.exception_rate = 1.5; // > 1.0
2780        let result = validate_config(&config);
2781        assert!(result.is_err());
2782        assert!(result.unwrap_err().to_string().contains("exception_rate"));
2783    }
2784
2785    #[test]
2786    fn test_approval_rejection_plus_revision_exceeds_one_rejected() {
2787        let mut config = minimal_valid_config();
2788        config.approval.enabled = true;
2789        config.approval.rejection_rate = 0.6;
2790        config.approval.revision_rate = 0.6; // Sum > 1.0
2791        let result = validate_config(&config);
2792        assert!(result.is_err());
2793        assert!(result
2794            .unwrap_err()
2795            .to_string()
2796            .contains("must not exceed 1.0"));
2797    }
2798
2799    #[test]
2800    fn test_master_data_intercompany_percent_invalid() {
2801        let mut config = minimal_valid_config();
2802        config.master_data.vendors.intercompany_percent = 1.5; // > 1.0
2803        let result = validate_config(&config);
2804        assert!(result.is_err());
2805        assert!(result
2806            .unwrap_err()
2807            .to_string()
2808            .contains("intercompany_percent"));
2809    }
2810
2811    #[test]
2812    fn test_balance_gross_margin_invalid() {
2813        let mut config = minimal_valid_config();
2814        config.balance.target_gross_margin = 1.5; // > 1.0
2815        let result = validate_config(&config);
2816        assert!(result.is_err());
2817        assert!(result
2818            .unwrap_err()
2819            .to_string()
2820            .contains("target_gross_margin"));
2821    }
2822
2823    #[test]
2824    fn test_negative_volume_weight_rejected() {
2825        let mut config = minimal_valid_config();
2826        config.companies[0].volume_weight = -0.5;
2827        let result = validate_config(&config);
2828        assert!(result.is_err());
2829        assert!(result.unwrap_err().to_string().contains("volume_weight"));
2830    }
2831
2832    #[test]
2833    fn test_benford_tolerance_invalid() {
2834        let mut config = minimal_valid_config();
2835        config.transactions.benford.tolerance = 1.5; // > 1.0
2836        let result = validate_config(&config);
2837        assert!(result.is_err());
2838        assert!(result
2839            .unwrap_err()
2840            .to_string()
2841            .contains("benford.tolerance"));
2842    }
2843
2844    #[test]
2845    fn test_batch_size_zero_rejected() {
2846        let mut config = minimal_valid_config();
2847        config.output.batch_size = 0;
2848        let result = validate_config(&config);
2849        assert!(result.is_err());
2850        assert!(result.unwrap_err().to_string().contains("batch_size"));
2851    }
2852
2853    // ==========================================================================
2854    // Temporal Patterns Validation Tests
2855    // ==========================================================================
2856
2857    #[test]
2858    fn test_temporal_patterns_disabled_passes() {
2859        let mut config = minimal_valid_config();
2860        config.temporal_patterns.enabled = false;
2861        assert!(validate_config(&config).is_ok());
2862    }
2863
2864    #[test]
2865    fn test_temporal_patterns_enabled_with_defaults_passes() {
2866        let mut config = minimal_valid_config();
2867        config.temporal_patterns.enabled = true;
2868        assert!(validate_config(&config).is_ok());
2869    }
2870
2871    #[test]
2872    fn test_business_day_invalid_half_day_policy() {
2873        let mut config = minimal_valid_config();
2874        config.temporal_patterns.enabled = true;
2875        config.temporal_patterns.business_days.enabled = true;
2876        config.temporal_patterns.business_days.half_day_policy = "invalid_policy".to_string();
2877        let result = validate_config(&config);
2878        assert!(result.is_err());
2879        assert!(result.unwrap_err().to_string().contains("half_day_policy"));
2880    }
2881
2882    #[test]
2883    fn test_business_day_valid_half_day_policies() {
2884        let mut config = minimal_valid_config();
2885        config.temporal_patterns.enabled = true;
2886        config.temporal_patterns.business_days.enabled = true;
2887
2888        for policy in ["full_day", "half_day", "non_business_day"] {
2889            config.temporal_patterns.business_days.half_day_policy = policy.to_string();
2890            assert!(
2891                validate_config(&config).is_ok(),
2892                "Expected '{}' to be valid",
2893                policy
2894            );
2895        }
2896    }
2897
2898    #[test]
2899    fn test_business_day_invalid_month_end_convention() {
2900        let mut config = minimal_valid_config();
2901        config.temporal_patterns.enabled = true;
2902        config.temporal_patterns.business_days.enabled = true;
2903        config.temporal_patterns.business_days.month_end_convention = "invalid".to_string();
2904        let result = validate_config(&config);
2905        assert!(result.is_err());
2906        assert!(result
2907            .unwrap_err()
2908            .to_string()
2909            .contains("month_end_convention"));
2910    }
2911
2912    #[test]
2913    fn test_period_end_invalid_model() {
2914        let mut config = minimal_valid_config();
2915        config.temporal_patterns.enabled = true;
2916        config.temporal_patterns.period_end.model = Some("invalid_model".to_string());
2917        let result = validate_config(&config);
2918        assert!(result.is_err());
2919        assert!(result.unwrap_err().to_string().contains("model"));
2920    }
2921
2922    #[test]
2923    fn test_period_end_valid_models() {
2924        let mut config = minimal_valid_config();
2925        config.temporal_patterns.enabled = true;
2926
2927        for model in ["flat", "exponential", "daily_profile", "extended_crunch"] {
2928            config.temporal_patterns.period_end.model = Some(model.to_string());
2929            assert!(
2930                validate_config(&config).is_ok(),
2931                "Expected model '{}' to be valid",
2932                model
2933            );
2934        }
2935    }
2936
2937    #[test]
2938    fn test_period_end_invalid_decay_rate() {
2939        let mut config = minimal_valid_config();
2940        config.temporal_patterns.enabled = true;
2941        config.temporal_patterns.period_end.model = Some("exponential".to_string());
2942        config.temporal_patterns.period_end.month_end = Some(PeriodEndModelSchemaConfig {
2943            inherit_from: None,
2944            additional_multiplier: None,
2945            start_day: Some(-10),
2946            base_multiplier: Some(1.0),
2947            peak_multiplier: Some(3.5),
2948            decay_rate: Some(1.5), // Invalid: > 1.0
2949            sustained_high_days: None,
2950        });
2951        let result = validate_config(&config);
2952        assert!(result.is_err());
2953        assert!(result.unwrap_err().to_string().contains("decay_rate"));
2954    }
2955
2956    #[test]
2957    fn test_period_end_negative_multiplier() {
2958        let mut config = minimal_valid_config();
2959        config.temporal_patterns.enabled = true;
2960        config.temporal_patterns.period_end.month_end = Some(PeriodEndModelSchemaConfig {
2961            inherit_from: None,
2962            additional_multiplier: None,
2963            start_day: Some(-10),
2964            base_multiplier: Some(1.0),
2965            peak_multiplier: Some(-1.0), // Invalid: negative
2966            decay_rate: Some(0.3),
2967            sustained_high_days: None,
2968        });
2969        let result = validate_config(&config);
2970        assert!(result.is_err());
2971        assert!(result.unwrap_err().to_string().contains("multiplier"));
2972    }
2973
2974    #[test]
2975    fn test_processing_lag_negative_mu_allowed() {
2976        // Note: For log-normal distributions, mu (log-scale mean) can be any real number
2977        // including negative values. This test verifies that negative mu is allowed.
2978        let mut config = minimal_valid_config();
2979        config.temporal_patterns.enabled = true;
2980        config.temporal_patterns.processing_lags.enabled = true;
2981        config.temporal_patterns.processing_lags.sales_order_lag =
2982            Some(LagDistributionSchemaConfig {
2983                mu: -1.0, // Valid: log-normal mu can be negative
2984                sigma: 0.8,
2985                min_hours: None,
2986                max_hours: None,
2987            });
2988        assert!(validate_config(&config).is_ok());
2989    }
2990
2991    #[test]
2992    fn test_processing_lag_negative_sigma() {
2993        let mut config = minimal_valid_config();
2994        config.temporal_patterns.enabled = true;
2995        config.temporal_patterns.processing_lags.enabled = true;
2996        config.temporal_patterns.processing_lags.goods_receipt_lag =
2997            Some(LagDistributionSchemaConfig {
2998                mu: 1.5,
2999                sigma: -0.5, // Invalid: negative
3000                min_hours: None,
3001                max_hours: None,
3002            });
3003        let result = validate_config(&config);
3004        assert!(result.is_err());
3005        assert!(result.unwrap_err().to_string().contains("sigma"));
3006    }
3007
3008    #[test]
3009    fn test_fiscal_calendar_invalid_year_start_month() {
3010        let mut config = minimal_valid_config();
3011        config.temporal_patterns.enabled = true;
3012        config.temporal_patterns.fiscal_calendar.enabled = true;
3013        config.temporal_patterns.fiscal_calendar.calendar_type = "custom".to_string();
3014        config.temporal_patterns.fiscal_calendar.year_start_month = Some(13); // Invalid
3015        let result = validate_config(&config);
3016        assert!(result.is_err());
3017        assert!(result.unwrap_err().to_string().contains("year_start_month"));
3018    }
3019
3020    #[test]
3021    fn test_fiscal_calendar_invalid_year_start_day() {
3022        let mut config = minimal_valid_config();
3023        config.temporal_patterns.enabled = true;
3024        config.temporal_patterns.fiscal_calendar.enabled = true;
3025        config.temporal_patterns.fiscal_calendar.calendar_type = "custom".to_string();
3026        config.temporal_patterns.fiscal_calendar.year_start_month = Some(2);
3027        config.temporal_patterns.fiscal_calendar.year_start_day = Some(32); // Invalid
3028        let result = validate_config(&config);
3029        assert!(result.is_err());
3030        assert!(result.unwrap_err().to_string().contains("year_start_day"));
3031    }
3032
3033    #[test]
3034    fn test_intraday_invalid_time_format() {
3035        let mut config = minimal_valid_config();
3036        config.temporal_patterns.enabled = true;
3037        config.temporal_patterns.intraday.enabled = true;
3038        config.temporal_patterns.intraday.segments = vec![IntraDaySegmentSchemaConfig {
3039            name: "test".to_string(),
3040            start: "25:00".to_string(), // Invalid hour
3041            end: "10:00".to_string(),
3042            multiplier: 1.5,
3043            posting_type: "both".to_string(),
3044        }];
3045        let result = validate_config(&config);
3046        assert!(result.is_err());
3047        assert!(result.unwrap_err().to_string().contains("HH:MM format"));
3048    }
3049
3050    #[test]
3051    fn test_intraday_invalid_posting_type() {
3052        let mut config = minimal_valid_config();
3053        config.temporal_patterns.enabled = true;
3054        config.temporal_patterns.intraday.enabled = true;
3055        config.temporal_patterns.intraday.segments = vec![IntraDaySegmentSchemaConfig {
3056            name: "test".to_string(),
3057            start: "08:00".to_string(),
3058            end: "10:00".to_string(),
3059            multiplier: 1.5,
3060            posting_type: "invalid".to_string(),
3061        }];
3062        let result = validate_config(&config);
3063        assert!(result.is_err());
3064        assert!(result.unwrap_err().to_string().contains("posting_type"));
3065    }
3066
3067    #[test]
3068    fn test_intraday_negative_multiplier() {
3069        let mut config = minimal_valid_config();
3070        config.temporal_patterns.enabled = true;
3071        config.temporal_patterns.intraday.enabled = true;
3072        config.temporal_patterns.intraday.segments = vec![IntraDaySegmentSchemaConfig {
3073            name: "test".to_string(),
3074            start: "08:00".to_string(),
3075            end: "10:00".to_string(),
3076            multiplier: -1.0, // Invalid
3077            posting_type: "both".to_string(),
3078        }];
3079        let result = validate_config(&config);
3080        assert!(result.is_err());
3081        assert!(result.unwrap_err().to_string().contains("multiplier"));
3082    }
3083
3084    #[test]
3085    fn test_timezone_invalid_default() {
3086        let mut config = minimal_valid_config();
3087        config.temporal_patterns.enabled = true;
3088        config.temporal_patterns.timezones.enabled = true;
3089        config.temporal_patterns.timezones.default_timezone = "Invalid/Timezone".to_string();
3090        let result = validate_config(&config);
3091        assert!(result.is_err());
3092        assert!(result.unwrap_err().to_string().contains("timezone"));
3093    }
3094
3095    #[test]
3096    fn test_timezone_valid_iana_names() {
3097        let mut config = minimal_valid_config();
3098        config.temporal_patterns.enabled = true;
3099        config.temporal_patterns.timezones.enabled = true;
3100        config.temporal_patterns.timezones.consolidation_timezone = "UTC".to_string();
3101
3102        for tz in [
3103            "America/New_York",
3104            "Europe/London",
3105            "Asia/Tokyo",
3106            "UTC",
3107            "Pacific/Auckland",
3108        ] {
3109            config.temporal_patterns.timezones.default_timezone = tz.to_string();
3110            let result = validate_config(&config);
3111            assert!(
3112                result.is_ok(),
3113                "Expected timezone '{}' to be valid, got error: {:?}",
3114                tz,
3115                result.err()
3116            );
3117        }
3118    }
3119
3120    #[test]
3121    fn test_timezone_invalid_entity_mapping() {
3122        let mut config = minimal_valid_config();
3123        config.temporal_patterns.enabled = true;
3124        config.temporal_patterns.timezones.enabled = true;
3125        config.temporal_patterns.timezones.entity_mappings = vec![EntityTimezoneMapping {
3126            pattern: "EU_*".to_string(),
3127            timezone: "Invalid/TZ".to_string(),
3128        }];
3129        let result = validate_config(&config);
3130        assert!(result.is_err());
3131        assert!(result.unwrap_err().to_string().contains("timezone"));
3132    }
3133}