Skip to main content

datasynth_test_utils/
assertions.rs

1//! Custom assertion macros for testing accounting invariants.
2
3use datasynth_core::models::JournalEntry;
4use rust_decimal::Decimal;
5
6/// Assert that a journal entry is balanced (debits equal credits).
7#[macro_export]
8macro_rules! assert_balanced {
9    ($entry:expr) => {{
10        let entry = &$entry;
11        let total_debits: rust_decimal::Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
12        let total_credits: rust_decimal::Decimal =
13            entry.lines.iter().map(|l| l.credit_amount).sum();
14        assert_eq!(
15            total_debits, total_credits,
16            "Journal entry is not balanced: debits={}, credits={}",
17            total_debits, total_credits
18        );
19    }};
20}
21
22/// Assert that all journal entries in a collection are balanced.
23#[macro_export]
24macro_rules! assert_all_balanced {
25    ($entries:expr) => {{
26        for (i, entry) in $entries.iter().enumerate() {
27            let total_debits: rust_decimal::Decimal =
28                entry.lines.iter().map(|l| l.debit_amount).sum();
29            let total_credits: rust_decimal::Decimal =
30                entry.lines.iter().map(|l| l.credit_amount).sum();
31            assert_eq!(
32                total_debits, total_credits,
33                "Journal entry {} is not balanced: debits={}, credits={}",
34                i, total_debits, total_credits
35            );
36        }
37    }};
38}
39
40/// Assert that an amount follows Benford's Law distribution within tolerance.
41/// This checks if the first digit distribution matches expected frequencies.
42#[macro_export]
43macro_rules! assert_benford_compliant {
44    ($amounts:expr, $tolerance:expr) => {{
45        let amounts = &$amounts;
46        let expected = [0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046];
47        let mut counts = [0u64; 9];
48        let mut total = 0u64;
49
50        for amount in amounts.iter() {
51            if *amount > rust_decimal::Decimal::ZERO {
52                let first_digit = amount
53                    .to_string()
54                    .chars()
55                    .find(|c| c.is_ascii_digit() && *c != '0')
56                    .map(|c| c.to_digit(10).unwrap() as usize);
57
58                if let Some(d) = first_digit {
59                    if d >= 1 && d <= 9 {
60                        counts[d - 1] += 1;
61                        total += 1;
62                    }
63                }
64            }
65        }
66
67        if total > 0 {
68            for (i, (count, exp)) in counts.iter().zip(expected.iter()).enumerate() {
69                let observed = *count as f64 / total as f64;
70                let diff = (observed - exp).abs();
71                assert!(
72                    diff < $tolerance,
73                    "Benford's Law violation for digit {}: observed={:.4}, expected={:.4}, diff={:.4}",
74                    i + 1,
75                    observed,
76                    exp,
77                    diff
78                );
79            }
80        }
81    }};
82}
83
84/// Check if a journal entry is balanced.
85pub fn is_balanced(entry: &JournalEntry) -> bool {
86    let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
87    let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
88    total_debits == total_credits
89}
90
91/// Calculate the imbalance of a journal entry.
92pub fn calculate_imbalance(entry: &JournalEntry) -> Decimal {
93    let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
94    let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
95    total_debits - total_credits
96}
97
98/// Check if amounts follow Benford's Law distribution.
99/// Returns the chi-squared statistic and whether it passes the test at p < 0.05.
100pub fn check_benford_distribution(amounts: &[Decimal]) -> (f64, bool) {
101    let expected = [
102        0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
103    ];
104    let mut counts = [0u64; 9];
105    let mut total = 0u64;
106
107    for amount in amounts.iter() {
108        if *amount > Decimal::ZERO {
109            let first_digit = amount
110                .to_string()
111                .chars()
112                .find(|c| c.is_ascii_digit() && *c != '0')
113                .map(|c| c.to_digit(10).unwrap() as usize);
114
115            if let Some(d) = first_digit {
116                if (1..=9).contains(&d) {
117                    counts[d - 1] += 1;
118                    total += 1;
119                }
120            }
121        }
122    }
123
124    if total == 0 {
125        return (0.0, true);
126    }
127
128    // Calculate chi-squared statistic
129    let mut chi_squared = 0.0;
130    for (count, exp) in counts.iter().zip(expected.iter()) {
131        let expected_count = exp * total as f64;
132        if expected_count > 0.0 {
133            let diff = *count as f64 - expected_count;
134            chi_squared += diff * diff / expected_count;
135        }
136    }
137
138    // Critical value for chi-squared with 8 degrees of freedom at p < 0.05 is 15.507
139    // At p < 0.01 is 20.090
140    let passes = chi_squared < 20.090;
141
142    (chi_squared, passes)
143}
144
145/// Check that the accounting equation holds: Assets = Liabilities + Equity
146pub fn check_accounting_equation(
147    total_assets: Decimal,
148    total_liabilities: Decimal,
149    total_equity: Decimal,
150) -> bool {
151    total_assets == total_liabilities + total_equity
152}
153
154/// Verify trial balance is balanced (total debits = total credits).
155pub fn check_trial_balance(debit_balances: &[Decimal], credit_balances: &[Decimal]) -> bool {
156    let total_debits: Decimal = debit_balances.iter().copied().sum();
157    let total_credits: Decimal = credit_balances.iter().copied().sum();
158    total_debits == total_credits
159}
160
161// =============================================================================
162// Enhanced Test Assertions
163// =============================================================================
164
165/// Assert that amounts pass Benford's Law chi-squared test.
166/// Uses the chi-squared statistic with configurable threshold.
167#[macro_export]
168macro_rules! assert_benford_passes {
169    ($amounts:expr, $threshold:expr) => {{
170        let (chi_squared, passes) = $crate::assertions::check_benford_distribution(&$amounts);
171        assert!(
172            passes || chi_squared < $threshold,
173            "Benford's Law test failed: chi-squared={:.4}, threshold={}",
174            chi_squared,
175            $threshold
176        );
177    }};
178    ($amounts:expr) => {{
179        let (chi_squared, passes) = $crate::assertions::check_benford_distribution(&$amounts);
180        assert!(
181            passes,
182            "Benford's Law test failed: chi-squared={:.4}, p < 0.01 threshold=20.090",
183            chi_squared
184        );
185    }};
186}
187
188/// Balance snapshot for coherence testing.
189#[derive(Debug, Clone)]
190pub struct BalanceSnapshot {
191    /// Total assets
192    pub assets: Decimal,
193    /// Total liabilities
194    pub liabilities: Decimal,
195    /// Total equity
196    pub equity: Decimal,
197    /// Period identifier
198    pub period: String,
199}
200
201impl BalanceSnapshot {
202    /// Create a new balance snapshot.
203    pub fn new(assets: Decimal, liabilities: Decimal, equity: Decimal, period: &str) -> Self {
204        Self {
205            assets,
206            liabilities,
207            equity,
208            period: period.into(),
209        }
210    }
211
212    /// Check if the accounting equation holds within tolerance.
213    pub fn is_coherent(&self, tolerance: Decimal) -> bool {
214        let diff = self.assets - (self.liabilities + self.equity);
215        diff.abs() <= tolerance
216    }
217}
218
219/// Assert that balance snapshots maintain accounting equation coherence.
220/// Checks that Assets = Liabilities + Equity within tolerance.
221#[macro_export]
222macro_rules! assert_balance_coherent {
223    ($snapshots:expr, $tolerance:expr) => {{
224        let tolerance =
225            rust_decimal::Decimal::try_from($tolerance).unwrap_or(rust_decimal::Decimal::ZERO);
226        for snapshot in $snapshots.iter() {
227            assert!(
228                snapshot.is_coherent(tolerance),
229                "Balance not coherent for period {}: assets={}, liabilities={}, equity={}, diff={}",
230                snapshot.period,
231                snapshot.assets,
232                snapshot.liabilities,
233                snapshot.equity,
234                snapshot.assets - (snapshot.liabilities + snapshot.equity)
235            );
236        }
237    }};
238}
239
240/// Subledger reconciliation data.
241#[derive(Debug, Clone)]
242pub struct SubledgerReconciliation {
243    /// Subledger name (AR, AP, FA, Inventory)
244    pub subledger: String,
245    /// Total from subledger
246    pub subledger_total: Decimal,
247    /// GL control account balance
248    pub gl_balance: Decimal,
249    /// Period
250    pub period: String,
251}
252
253impl SubledgerReconciliation {
254    /// Create new reconciliation data.
255    pub fn new(
256        subledger: &str,
257        subledger_total: Decimal,
258        gl_balance: Decimal,
259        period: &str,
260    ) -> Self {
261        Self {
262            subledger: subledger.into(),
263            subledger_total,
264            gl_balance,
265            period: period.into(),
266        }
267    }
268
269    /// Check if subledger reconciles to GL within tolerance.
270    pub fn is_reconciled(&self, tolerance: Decimal) -> bool {
271        let diff = (self.subledger_total - self.gl_balance).abs();
272        diff <= tolerance
273    }
274
275    /// Get the reconciliation difference.
276    pub fn difference(&self) -> Decimal {
277        self.subledger_total - self.gl_balance
278    }
279}
280
281/// Assert that subledgers reconcile to GL control accounts.
282#[macro_export]
283macro_rules! assert_subledger_reconciled {
284    ($reconciliations:expr, $tolerance:expr) => {{
285        let tolerance =
286            rust_decimal::Decimal::try_from($tolerance).unwrap_or(rust_decimal::Decimal::ZERO);
287        for recon in $reconciliations.iter() {
288            assert!(
289                recon.is_reconciled(tolerance),
290                "Subledger {} not reconciled for period {}: subledger={}, gl={}, diff={}",
291                recon.subledger,
292                recon.period,
293                recon.subledger_total,
294                recon.gl_balance,
295                recon.difference()
296            );
297        }
298    }};
299}
300
301/// Document chain validation result.
302#[derive(Debug, Clone)]
303pub struct DocumentChainResult {
304    /// Chain identifier
305    pub chain_id: String,
306    /// Whether chain is complete
307    pub is_complete: bool,
308    /// Missing steps (if any)
309    pub missing_steps: Vec<String>,
310    /// Total steps expected
311    pub expected_steps: usize,
312    /// Actual steps found
313    pub actual_steps: usize,
314}
315
316impl DocumentChainResult {
317    /// Create a new chain result.
318    pub fn new(chain_id: &str, expected_steps: usize, actual_steps: usize) -> Self {
319        Self {
320            chain_id: chain_id.into(),
321            is_complete: actual_steps >= expected_steps,
322            missing_steps: Vec::new(),
323            expected_steps,
324            actual_steps,
325        }
326    }
327
328    /// Create a complete chain result.
329    pub fn complete(chain_id: &str, steps: usize) -> Self {
330        Self::new(chain_id, steps, steps)
331    }
332
333    /// Create an incomplete chain result.
334    pub fn incomplete(
335        chain_id: &str,
336        expected: usize,
337        actual: usize,
338        missing: Vec<String>,
339    ) -> Self {
340        Self {
341            chain_id: chain_id.into(),
342            is_complete: false,
343            missing_steps: missing,
344            expected_steps: expected,
345            actual_steps: actual,
346        }
347    }
348
349    /// Get completion rate.
350    pub fn completion_rate(&self) -> f64 {
351        if self.expected_steps == 0 {
352            1.0
353        } else {
354            self.actual_steps as f64 / self.expected_steps as f64
355        }
356    }
357}
358
359/// Check document chain completeness rate.
360pub fn check_document_chain_completeness(chains: &[DocumentChainResult]) -> (f64, usize, usize) {
361    if chains.is_empty() {
362        return (1.0, 0, 0);
363    }
364
365    let complete_count = chains.iter().filter(|c| c.is_complete).count();
366    let total_count = chains.len();
367    let rate = complete_count as f64 / total_count as f64;
368
369    (rate, complete_count, total_count)
370}
371
372/// Assert that document chains meet completeness threshold.
373#[macro_export]
374macro_rules! assert_document_chain_complete {
375    ($chains:expr, $threshold:expr) => {{
376        let (rate, complete, total) =
377            $crate::assertions::check_document_chain_completeness(&$chains);
378        assert!(
379            rate >= $threshold,
380            "Document chain completeness {:.2}% below threshold {:.2}%: {}/{} complete",
381            rate * 100.0,
382            $threshold * 100.0,
383            complete,
384            total
385        );
386
387        // Also report incomplete chains for debugging
388        for chain in $chains.iter().filter(|c| !c.is_complete) {
389            eprintln!(
390                "Incomplete chain {}: {}/{} steps, missing: {:?}",
391                chain.chain_id, chain.actual_steps, chain.expected_steps, chain.missing_steps
392            );
393        }
394    }};
395}
396
397/// Fidelity comparison result.
398#[derive(Debug, Clone)]
399pub struct FidelityResult {
400    /// Overall fidelity score (0.0 - 1.0)
401    pub overall_score: f64,
402    /// Statistical fidelity (distribution similarity)
403    pub statistical_score: f64,
404    /// Schema fidelity (structure match)
405    pub schema_score: f64,
406    /// Correlation fidelity (relationship preservation)
407    pub correlation_score: f64,
408    /// Whether fidelity passes threshold
409    pub passes: bool,
410    /// Threshold used
411    pub threshold: f64,
412}
413
414impl FidelityResult {
415    /// Create a new fidelity result.
416    pub fn new(statistical: f64, schema: f64, correlation: f64, threshold: f64) -> Self {
417        // Weighted average: statistical 50%, schema 25%, correlation 25%
418        let overall = statistical * 0.50 + schema * 0.25 + correlation * 0.25;
419
420        Self {
421            overall_score: overall,
422            statistical_score: statistical,
423            schema_score: schema,
424            correlation_score: correlation,
425            passes: overall >= threshold,
426            threshold,
427        }
428    }
429
430    /// Create a perfect fidelity result (for self-comparison).
431    pub fn perfect(threshold: f64) -> Self {
432        Self::new(1.0, 1.0, 1.0, threshold)
433    }
434}
435
436/// Check fidelity between synthetic data and fingerprint.
437pub fn check_fidelity(
438    statistical_score: f64,
439    schema_score: f64,
440    correlation_score: f64,
441    threshold: f64,
442) -> FidelityResult {
443    FidelityResult::new(
444        statistical_score,
445        schema_score,
446        correlation_score,
447        threshold,
448    )
449}
450
451/// Assert that fidelity passes the threshold.
452#[macro_export]
453macro_rules! assert_fidelity_passes {
454    ($result:expr) => {{
455        assert!(
456            $result.passes,
457            "Fidelity check failed: overall={:.4} < threshold={:.4}\n  \
458             statistical={:.4}, schema={:.4}, correlation={:.4}",
459            $result.overall_score,
460            $result.threshold,
461            $result.statistical_score,
462            $result.schema_score,
463            $result.correlation_score
464        );
465    }};
466    ($statistical:expr, $schema:expr, $correlation:expr, $threshold:expr) => {{
467        let result =
468            $crate::assertions::check_fidelity($statistical, $schema, $correlation, $threshold);
469        assert!(
470            result.passes,
471            "Fidelity check failed: overall={:.4} < threshold={:.4}\n  \
472             statistical={:.4}, schema={:.4}, correlation={:.4}",
473            result.overall_score,
474            result.threshold,
475            result.statistical_score,
476            result.schema_score,
477            result.correlation_score
478        );
479    }};
480}
481
482/// Convenience function to compute Mean Absolute Deviation for Benford analysis.
483pub fn benford_mad(amounts: &[Decimal]) -> f64 {
484    let expected = [
485        0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
486    ];
487    let mut counts = [0u64; 9];
488    let mut total = 0u64;
489
490    for amount in amounts.iter() {
491        if *amount > Decimal::ZERO {
492            let first_digit = amount
493                .to_string()
494                .chars()
495                .find(|c| c.is_ascii_digit() && *c != '0')
496                .and_then(|c| c.to_digit(10))
497                .map(|d| d as usize);
498
499            if let Some(d) = first_digit {
500                if (1..=9).contains(&d) {
501                    counts[d - 1] += 1;
502                    total += 1;
503                }
504            }
505        }
506    }
507
508    if total == 0 {
509        return 0.0;
510    }
511
512    // Calculate Mean Absolute Deviation
513    let mut mad = 0.0;
514    for (count, exp) in counts.iter().zip(expected.iter()) {
515        let observed = *count as f64 / total as f64;
516        mad += (observed - exp).abs();
517    }
518
519    mad / 9.0
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use crate::fixtures::*;
526
527    #[test]
528    fn test_is_balanced() {
529        let entry = balanced_journal_entry(Decimal::new(10000, 2));
530        assert!(is_balanced(&entry));
531    }
532
533    #[test]
534    fn test_is_not_balanced() {
535        let entry = unbalanced_journal_entry();
536        assert!(!is_balanced(&entry));
537    }
538
539    #[test]
540    fn test_calculate_imbalance_balanced() {
541        let entry = balanced_journal_entry(Decimal::new(10000, 2));
542        assert_eq!(calculate_imbalance(&entry), Decimal::ZERO);
543    }
544
545    #[test]
546    fn test_calculate_imbalance_unbalanced() {
547        let entry = unbalanced_journal_entry();
548        let imbalance = calculate_imbalance(&entry);
549        assert_ne!(imbalance, Decimal::ZERO);
550    }
551
552    #[test]
553    fn test_check_accounting_equation() {
554        // Assets = 1000, Liabilities = 600, Equity = 400
555        assert!(check_accounting_equation(
556            Decimal::new(1000, 0),
557            Decimal::new(600, 0),
558            Decimal::new(400, 0)
559        ));
560
561        // Unbalanced: Assets = 1000, Liabilities = 600, Equity = 300
562        assert!(!check_accounting_equation(
563            Decimal::new(1000, 0),
564            Decimal::new(600, 0),
565            Decimal::new(300, 0)
566        ));
567    }
568
569    #[test]
570    fn test_check_trial_balance() {
571        let debits = vec![Decimal::new(1000, 0), Decimal::new(500, 0)];
572        let credits = vec![Decimal::new(1500, 0)];
573        assert!(check_trial_balance(&debits, &credits));
574
575        let unbalanced_credits = vec![Decimal::new(1000, 0)];
576        assert!(!check_trial_balance(&debits, &unbalanced_credits));
577    }
578
579    #[test]
580    fn test_benford_distribution_perfect() {
581        // Create a distribution that follows Benford's Law
582        let mut amounts = Vec::new();
583        let expected_counts = [301, 176, 125, 97, 79, 67, 58, 51, 46]; // Per 1000
584
585        for (digit, count) in expected_counts.iter().enumerate() {
586            let base = Decimal::new((digit + 1) as i64, 0);
587            for _ in 0..*count {
588                amounts.push(base);
589            }
590        }
591
592        let (chi_squared, passes) = check_benford_distribution(&amounts);
593        assert!(passes, "Chi-squared: {}", chi_squared);
594    }
595
596    #[test]
597    fn test_assert_balanced_macro() {
598        let entry = balanced_journal_entry(Decimal::new(10000, 2));
599        assert_balanced!(entry); // Should not panic
600    }
601
602    #[test]
603    fn test_assert_all_balanced_macro() {
604        let entries = [
605            balanced_journal_entry(Decimal::new(10000, 2)),
606            balanced_journal_entry(Decimal::new(20000, 2)),
607            balanced_journal_entry(Decimal::new(30000, 2)),
608        ];
609        assert_all_balanced!(entries); // Should not panic
610    }
611
612    // =============================================================================
613    // Tests for new enhanced assertions
614    // =============================================================================
615
616    #[test]
617    fn test_balance_snapshot_coherent() {
618        let snapshot = BalanceSnapshot::new(
619            Decimal::new(1000, 0),
620            Decimal::new(600, 0),
621            Decimal::new(400, 0),
622            "2025-01",
623        );
624        assert!(snapshot.is_coherent(Decimal::ZERO));
625    }
626
627    #[test]
628    fn test_balance_snapshot_incoherent() {
629        let snapshot = BalanceSnapshot::new(
630            Decimal::new(1000, 0),
631            Decimal::new(600, 0),
632            Decimal::new(300, 0), // Assets != L + E
633            "2025-01",
634        );
635        assert!(!snapshot.is_coherent(Decimal::ZERO));
636    }
637
638    #[test]
639    fn test_balance_snapshot_with_tolerance() {
640        let snapshot = BalanceSnapshot::new(
641            Decimal::new(1001, 0), // Off by 1
642            Decimal::new(600, 0),
643            Decimal::new(400, 0),
644            "2025-01",
645        );
646        assert!(!snapshot.is_coherent(Decimal::ZERO));
647        assert!(snapshot.is_coherent(Decimal::new(1, 0)));
648        assert!(snapshot.is_coherent(Decimal::new(5, 0)));
649    }
650
651    #[test]
652    fn test_assert_balance_coherent_macro() {
653        let snapshots = [
654            BalanceSnapshot::new(
655                Decimal::new(1000, 0),
656                Decimal::new(600, 0),
657                Decimal::new(400, 0),
658                "2025-01",
659            ),
660            BalanceSnapshot::new(
661                Decimal::new(1200, 0),
662                Decimal::new(700, 0),
663                Decimal::new(500, 0),
664                "2025-02",
665            ),
666        ];
667        assert_balance_coherent!(snapshots, 0.0);
668    }
669
670    #[test]
671    fn test_subledger_reconciliation() {
672        let recon = SubledgerReconciliation::new(
673            "AR",
674            Decimal::new(50000, 0),
675            Decimal::new(50000, 0),
676            "2025-01",
677        );
678        assert!(recon.is_reconciled(Decimal::ZERO));
679        assert_eq!(recon.difference(), Decimal::ZERO);
680    }
681
682    #[test]
683    fn test_subledger_reconciliation_with_tolerance() {
684        let recon = SubledgerReconciliation::new(
685            "AP",
686            Decimal::new(50010, 0), // Off by 10
687            Decimal::new(50000, 0),
688            "2025-01",
689        );
690        assert!(!recon.is_reconciled(Decimal::new(5, 0)));
691        assert!(recon.is_reconciled(Decimal::new(10, 0)));
692        assert!(recon.is_reconciled(Decimal::new(100, 0)));
693    }
694
695    #[test]
696    fn test_assert_subledger_reconciled_macro() {
697        let reconciliations = [
698            SubledgerReconciliation::new(
699                "AR",
700                Decimal::new(50000, 0),
701                Decimal::new(50000, 0),
702                "2025-01",
703            ),
704            SubledgerReconciliation::new(
705                "AP",
706                Decimal::new(30000, 0),
707                Decimal::new(30000, 0),
708                "2025-01",
709            ),
710        ];
711        assert_subledger_reconciled!(reconciliations, 0.0);
712    }
713
714    #[test]
715    fn test_document_chain_complete() {
716        let chain = DocumentChainResult::complete("PO-001", 5);
717        assert!(chain.is_complete);
718        assert_eq!(chain.completion_rate(), 1.0);
719    }
720
721    #[test]
722    fn test_document_chain_incomplete() {
723        let chain =
724            DocumentChainResult::incomplete("PO-002", 5, 3, vec!["Payment".into(), "Close".into()]);
725        assert!(!chain.is_complete);
726        assert_eq!(chain.completion_rate(), 0.6);
727    }
728
729    #[test]
730    fn test_check_document_chain_completeness() {
731        let chains = vec![
732            DocumentChainResult::complete("PO-001", 5),
733            DocumentChainResult::complete("PO-002", 5),
734            DocumentChainResult::incomplete("PO-003", 5, 3, vec!["Payment".into()]),
735        ];
736
737        let (rate, complete, total) = check_document_chain_completeness(&chains);
738        assert_eq!(complete, 2);
739        assert_eq!(total, 3);
740        assert!((rate - 0.6667).abs() < 0.01);
741    }
742
743    #[test]
744    fn test_assert_document_chain_complete_macro() {
745        let chains = vec![
746            DocumentChainResult::complete("PO-001", 5),
747            DocumentChainResult::complete("PO-002", 5),
748            DocumentChainResult::complete("PO-003", 5),
749        ];
750        assert_document_chain_complete!(chains, 0.9);
751    }
752
753    #[test]
754    fn test_fidelity_result() {
755        let result = FidelityResult::new(0.95, 1.0, 0.90, 0.80);
756
757        // Weighted: 0.95 * 0.5 + 1.0 * 0.25 + 0.90 * 0.25 = 0.475 + 0.25 + 0.225 = 0.95
758        assert!((result.overall_score - 0.95).abs() < 0.001);
759        assert!(result.passes);
760    }
761
762    #[test]
763    fn test_fidelity_result_fails() {
764        let result = FidelityResult::new(0.50, 0.50, 0.50, 0.80);
765
766        // Weighted: 0.50 * 0.5 + 0.50 * 0.25 + 0.50 * 0.25 = 0.25 + 0.125 + 0.125 = 0.50
767        assert!((result.overall_score - 0.50).abs() < 0.001);
768        assert!(!result.passes);
769    }
770
771    #[test]
772    fn test_fidelity_perfect() {
773        let result = FidelityResult::perfect(0.90);
774        assert_eq!(result.overall_score, 1.0);
775        assert!(result.passes);
776    }
777
778    #[test]
779    fn test_assert_fidelity_passes_macro() {
780        let result = FidelityResult::new(0.95, 1.0, 0.90, 0.80);
781        assert_fidelity_passes!(result);
782    }
783
784    #[test]
785    fn test_assert_fidelity_passes_inline() {
786        assert_fidelity_passes!(0.95, 1.0, 0.90, 0.80);
787    }
788
789    #[test]
790    fn test_benford_mad() {
791        // Create a perfect Benford distribution
792        let mut amounts = Vec::new();
793        let expected_counts = [301, 176, 125, 97, 79, 67, 58, 51, 46];
794
795        for (digit, count) in expected_counts.iter().enumerate() {
796            let base = Decimal::new((digit + 1) as i64, 0);
797            for _ in 0..*count {
798                amounts.push(base);
799            }
800        }
801
802        let mad = benford_mad(&amounts);
803        assert!(
804            mad < 0.01,
805            "Perfect Benford distribution should have very low MAD: {}",
806            mad
807        );
808    }
809
810    #[test]
811    fn test_benford_mad_uniform() {
812        // Create a uniform distribution (bad for Benford)
813        let mut amounts = Vec::new();
814        for digit in 1..=9 {
815            for _ in 0..100 {
816                amounts.push(Decimal::new(digit, 0));
817            }
818        }
819
820        let mad = benford_mad(&amounts);
821        assert!(
822            mad > 0.02,
823            "Uniform distribution should have high MAD: {}",
824            mad
825        );
826    }
827}