Skip to main content

datasynth_generators/audit/
finding_generator.rs

1//! Finding generator for audit engagements.
2//!
3//! Generates audit findings with condition/criteria/cause/effect structure,
4//! remediation plans, and cross-references per ISA 265.
5
6use chrono::Duration;
7use datasynth_core::utils::seeded_rng;
8use rand::Rng;
9use rand_chacha::ChaCha8Rng;
10use rust_decimal::Decimal;
11
12use datasynth_core::models::audit::{
13    Assertion, AuditEngagement, AuditFinding, FindingSeverity, FindingStatus, FindingType,
14    MilestoneStatus, RemediationPlan, RemediationStatus, Workpaper,
15};
16
17/// Configuration for finding generation.
18#[derive(Debug, Clone)]
19pub struct FindingGeneratorConfig {
20    /// Number of findings per engagement (min, max)
21    pub findings_per_engagement: (u32, u32),
22    /// Probability of material weakness
23    pub material_weakness_probability: f64,
24    /// Probability of significant deficiency
25    pub significant_deficiency_probability: f64,
26    /// Probability of misstatement finding
27    pub misstatement_probability: f64,
28    /// Probability of remediation plan
29    pub remediation_plan_probability: f64,
30    /// Probability of management agreement
31    pub management_agrees_probability: f64,
32    /// Misstatement amount range (min, max)
33    pub misstatement_range: (i64, i64),
34}
35
36impl Default for FindingGeneratorConfig {
37    fn default() -> Self {
38        Self {
39            findings_per_engagement: (3, 12),
40            material_weakness_probability: 0.05,
41            significant_deficiency_probability: 0.15,
42            misstatement_probability: 0.30,
43            remediation_plan_probability: 0.70,
44            management_agrees_probability: 0.85,
45            misstatement_range: (1_000, 500_000),
46        }
47    }
48}
49
50/// Generator for audit findings.
51pub struct FindingGenerator {
52    rng: ChaCha8Rng,
53    config: FindingGeneratorConfig,
54    finding_counter: u32,
55    fiscal_year: u16,
56}
57
58impl FindingGenerator {
59    /// Create a new generator with the given seed.
60    pub fn new(seed: u64) -> Self {
61        Self {
62            rng: seeded_rng(seed, 0),
63            config: FindingGeneratorConfig::default(),
64            finding_counter: 0,
65            fiscal_year: 2025,
66        }
67    }
68
69    /// Create a new generator with custom configuration.
70    pub fn with_config(seed: u64, config: FindingGeneratorConfig) -> Self {
71        Self {
72            rng: seeded_rng(seed, 0),
73            config,
74            finding_counter: 0,
75            fiscal_year: 2025,
76        }
77    }
78
79    /// Generate findings for an engagement.
80    pub fn generate_findings_for_engagement(
81        &mut self,
82        engagement: &AuditEngagement,
83        workpapers: &[Workpaper],
84        team_members: &[String],
85    ) -> Vec<AuditFinding> {
86        self.fiscal_year = engagement.fiscal_year;
87
88        let count = self.rng.random_range(
89            self.config.findings_per_engagement.0..=self.config.findings_per_engagement.1,
90        );
91
92        let mut findings = Vec::with_capacity(count as usize);
93
94        for _ in 0..count {
95            let finding = self.generate_finding(engagement, workpapers, team_members);
96            findings.push(finding);
97        }
98
99        findings
100    }
101
102    /// Generate a single finding.
103    pub fn generate_finding(
104        &mut self,
105        engagement: &AuditEngagement,
106        workpapers: &[Workpaper],
107        team_members: &[String],
108    ) -> AuditFinding {
109        self.finding_counter += 1;
110
111        let finding_type = self.select_finding_type();
112        let (title, account) = self.generate_finding_title(finding_type);
113
114        let mut finding = AuditFinding::new(engagement.engagement_id, finding_type, &title);
115
116        finding.finding_ref = format!("FIND-{}-{:03}", self.fiscal_year, self.finding_counter);
117
118        // Generate condition, criteria, cause, effect
119        let (condition, criteria, cause, effect) = self.generate_ccce(finding_type, &account);
120        finding = finding.with_details(&condition, &criteria, &cause, &effect);
121
122        // Generate recommendation
123        let recommendation = self.generate_recommendation(finding_type, &account);
124        finding = finding.with_recommendation(&recommendation);
125
126        // Set severity based on type
127        finding.severity = self.determine_severity(finding_type, &finding);
128
129        // Add monetary impact for misstatements
130        if self.is_misstatement_type(finding_type) {
131            let (factual, projected, judgmental) = self.generate_misstatement_amounts();
132            finding = finding.with_misstatement(factual, projected, judgmental);
133
134            if let Some(f) = factual {
135                finding = finding.with_monetary_impact(f);
136            }
137        }
138
139        // Add assertions and accounts
140        finding.assertions_affected = self.select_assertions(finding_type);
141        finding.accounts_affected = vec![account.clone()];
142        finding.process_areas = self.select_process_areas(&account);
143
144        // Link to workpapers
145        if !workpapers.is_empty() {
146            let wp_count = self.rng.random_range(1..=3.min(workpapers.len()));
147            for _ in 0..wp_count {
148                let idx = self.rng.random_range(0..workpapers.len());
149                finding.workpaper_refs.push(workpapers[idx].workpaper_id);
150            }
151        }
152
153        // Set identified by
154        let identifier = self.select_team_member(team_members, "senior");
155        finding.identified_by = identifier;
156        finding.identified_date =
157            engagement.fieldwork_start + Duration::days(self.rng.random_range(7..30));
158
159        // Maybe add review
160        if self.rng.random::<f64>() < 0.8 {
161            finding.reviewed_by = Some(self.select_team_member(team_members, "manager"));
162            finding.review_date =
163                Some(finding.identified_date + Duration::days(self.rng.random_range(3..10)));
164            finding.status = FindingStatus::PendingReview;
165        }
166
167        // Determine reporting requirements
168        finding.mark_for_reporting(
169            finding.finding_type.requires_sox_reporting() || finding.severity.score() >= 3,
170            finding.requires_governance_communication(),
171        );
172
173        // Maybe add management response
174        if self.rng.random::<f64>() < 0.7 {
175            let response_date =
176                finding.identified_date + Duration::days(self.rng.random_range(7..21));
177            let agrees = self.rng.random::<f64>() < self.config.management_agrees_probability;
178            let response = self.generate_management_response(finding_type, agrees);
179            finding.add_management_response(&response, agrees, response_date);
180
181            // Maybe add remediation plan
182            if agrees && self.rng.random::<f64>() < self.config.remediation_plan_probability {
183                let plan = self.generate_remediation_plan(&finding, &account);
184                finding.with_remediation_plan(plan);
185            }
186        }
187
188        finding
189    }
190
191    /// Select finding type based on probabilities.
192    fn select_finding_type(&mut self) -> FindingType {
193        let r: f64 = self.rng.random();
194
195        if r < self.config.material_weakness_probability {
196            FindingType::MaterialWeakness
197        } else if r < self.config.material_weakness_probability
198            + self.config.significant_deficiency_probability
199        {
200            FindingType::SignificantDeficiency
201        } else if r < self.config.material_weakness_probability
202            + self.config.significant_deficiency_probability
203            + self.config.misstatement_probability
204        {
205            if self.rng.random::<f64>() < 0.3 {
206                FindingType::MaterialMisstatement
207            } else {
208                FindingType::ImmaterialMisstatement
209            }
210        } else {
211            let other_types = [
212                FindingType::ControlDeficiency,
213                FindingType::ComplianceException,
214                FindingType::OtherMatter,
215                FindingType::ItDeficiency,
216                FindingType::ProcessImprovement,
217            ];
218            let idx = self.rng.random_range(0..other_types.len());
219            other_types[idx]
220        }
221    }
222
223    /// Generate finding title and related account.
224    fn generate_finding_title(&mut self, finding_type: FindingType) -> (String, String) {
225        match finding_type {
226            FindingType::MaterialWeakness => {
227                let titles = [
228                    (
229                        "Inadequate segregation of duties in revenue cycle",
230                        "Revenue",
231                    ),
232                    (
233                        "Lack of effective review of journal entries",
234                        "General Ledger",
235                    ),
236                    (
237                        "Insufficient IT general controls over financial applications",
238                        "IT Controls",
239                    ),
240                    (
241                        "Inadequate controls over financial close process",
242                        "Financial Close",
243                    ),
244                ];
245                let idx = self.rng.random_range(0..titles.len());
246                (titles[idx].0.into(), titles[idx].1.into())
247            }
248            FindingType::SignificantDeficiency => {
249                let titles = [
250                    (
251                        "Inadequate documentation of account reconciliations",
252                        "Accounts Receivable",
253                    ),
254                    (
255                        "Untimely review of vendor master file changes",
256                        "Accounts Payable",
257                    ),
258                    ("Incomplete fixed asset physical inventory", "Fixed Assets"),
259                    (
260                        "Lack of formal approval for manual journal entries",
261                        "General Ledger",
262                    ),
263                ];
264                let idx = self.rng.random_range(0..titles.len());
265                (titles[idx].0.into(), titles[idx].1.into())
266            }
267            FindingType::ControlDeficiency => {
268                let titles = [
269                    (
270                        "Missing secondary approval on expense reports",
271                        "Operating Expenses",
272                    ),
273                    ("Incomplete access review documentation", "IT Controls"),
274                    ("Delayed bank reconciliation preparation", "Cash"),
275                    ("Inconsistent inventory count procedures", "Inventory"),
276                ];
277                let idx = self.rng.random_range(0..titles.len());
278                (titles[idx].0.into(), titles[idx].1.into())
279            }
280            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
281                let titles = [
282                    ("Revenue cutoff error", "Revenue"),
283                    ("Inventory valuation adjustment", "Inventory"),
284                    (
285                        "Accounts receivable allowance understatement",
286                        "Accounts Receivable",
287                    ),
288                    ("Accrued liabilities understatement", "Accrued Liabilities"),
289                    ("Fixed asset depreciation calculation error", "Fixed Assets"),
290                ];
291                let idx = self.rng.random_range(0..titles.len());
292                (titles[idx].0.into(), titles[idx].1.into())
293            }
294            FindingType::ComplianceException => {
295                let titles = [
296                    ("Late filing of sales tax returns", "Tax"),
297                    ("Incomplete Form 1099 reporting", "Tax"),
298                    ("Non-compliance with debt covenant reporting", "Debt"),
299                ];
300                let idx = self.rng.random_range(0..titles.len());
301                (titles[idx].0.into(), titles[idx].1.into())
302            }
303            FindingType::ItDeficiency => {
304                let titles = [
305                    ("Excessive user access privileges", "IT Controls"),
306                    ("Inadequate password policy enforcement", "IT Controls"),
307                    ("Missing change management documentation", "IT Controls"),
308                    ("Incomplete disaster recovery testing", "IT Controls"),
309                ];
310                let idx = self.rng.random_range(0..titles.len());
311                (titles[idx].0.into(), titles[idx].1.into())
312            }
313            FindingType::OtherMatter | FindingType::ProcessImprovement => {
314                let titles = [
315                    (
316                        "Opportunity to improve month-end close efficiency",
317                        "Financial Close",
318                    ),
319                    (
320                        "Enhancement to vendor onboarding process",
321                        "Accounts Payable",
322                    ),
323                    (
324                        "Automation opportunity in reconciliation process",
325                        "General Ledger",
326                    ),
327                ];
328                let idx = self.rng.random_range(0..titles.len());
329                (titles[idx].0.into(), titles[idx].1.into())
330            }
331        }
332    }
333
334    /// Generate condition, criteria, cause, effect.
335    fn generate_ccce(
336        &mut self,
337        finding_type: FindingType,
338        account: &str,
339    ) -> (String, String, String, String) {
340        match finding_type {
341            FindingType::MaterialWeakness
342            | FindingType::SignificantDeficiency
343            | FindingType::ControlDeficiency => {
344                let condition = format!(
345                    "During our testing of {} controls, we noted that the control was not operating effectively. \
346                    Specifically, {} of {} items tested did not have evidence of the required control activity.",
347                    account,
348                    self.rng.random_range(2..8),
349                    self.rng.random_range(20..40)
350                );
351                let criteria = format!(
352                    "Company policy and SOX requirements mandate that all {account} transactions receive appropriate \
353                    review and approval prior to processing."
354                );
355                let cause = "Staffing constraints and competing priorities resulted in reduced focus on control execution.".into();
356                let effect = format!(
357                    "Transactions may be processed without appropriate oversight, increasing the risk of errors \
358                    or fraud in the {account} balance."
359                );
360                (condition, criteria, cause, effect)
361            }
362            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
363                let amount = self.rng.random_range(
364                    self.config.misstatement_range.0..self.config.misstatement_range.1,
365                );
366                let condition = format!(
367                    "Our testing identified a misstatement in {account} of approximately ${amount}. \
368                    The error resulted from incorrect application of accounting standards."
369                );
370                let criteria = "US GAAP and company accounting policy require accurate recording of all transactions.".into();
371                let cause =
372                    "Manual calculation error combined with inadequate review procedures.".into();
373                let effect = format!(
374                    "The {} balance was {} by ${}, which {}.",
375                    account,
376                    if self.rng.random::<bool>() {
377                        "overstated"
378                    } else {
379                        "understated"
380                    },
381                    amount,
382                    if finding_type == FindingType::MaterialMisstatement {
383                        "represents a material misstatement"
384                    } else {
385                        "is below materiality but has been communicated to management"
386                    }
387                );
388                (condition, criteria, cause, effect)
389            }
390            FindingType::ComplianceException => {
391                let condition = format!(
392                    "The Company did not comply with {account} regulatory requirements during the period under audit."
393                );
394                let criteria =
395                    "Applicable laws and regulations require timely and accurate compliance."
396                        .into();
397                let cause = "Lack of monitoring procedures to track compliance deadlines.".into();
398                let effect =
399                    "The Company may be subject to penalties or regulatory scrutiny.".into();
400                (condition, criteria, cause, effect)
401            }
402            _ => {
403                let condition =
404                    format!("We identified an opportunity to enhance the {account} process.");
405                let criteria =
406                    "Industry best practices suggest continuous improvement in control processes."
407                        .into();
408                let cause =
409                    "Current processes have not been updated to reflect operational changes."
410                        .into();
411                let effect =
412                    "Operational efficiency could be improved with process enhancements.".into();
413                (condition, criteria, cause, effect)
414            }
415        }
416    }
417
418    /// Generate recommendation.
419    fn generate_recommendation(&mut self, finding_type: FindingType, account: &str) -> String {
420        match finding_type {
421            FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
422                format!(
423                    "We recommend that management: (1) Implement additional review procedures for {account} transactions, \
424                    (2) Document all control activities contemporaneously, and \
425                    (3) Provide additional training to personnel responsible for control execution."
426                )
427            }
428            FindingType::ControlDeficiency => {
429                format!(
430                    "We recommend that management strengthen the {account} control by ensuring timely execution \
431                    and documentation of all required review activities."
432                )
433            }
434            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
435                "We recommend that management record the proposed adjusting entry and implement \
436                additional review procedures to prevent similar errors in future periods.".into()
437            }
438            FindingType::ComplianceException => {
439                "We recommend that management implement a compliance calendar with automated reminders \
440                and establish monitoring procedures to ensure timely compliance.".into()
441            }
442            FindingType::ItDeficiency => {
443                "We recommend that IT management review and remediate the identified access control \
444                weaknesses and implement periodic access certification procedures.".into()
445            }
446            _ => {
447                format!(
448                    "We recommend that management evaluate the {account} process for potential \
449                    efficiency improvements and implement changes as appropriate."
450                )
451            }
452        }
453    }
454
455    /// Determine severity based on finding type and other factors.
456    fn determine_severity(
457        &mut self,
458        finding_type: FindingType,
459        _finding: &AuditFinding,
460    ) -> FindingSeverity {
461        let base_severity = finding_type.default_severity();
462
463        // Maybe adjust severity
464        if self.rng.random::<f64>() < 0.2 {
465            match base_severity {
466                FindingSeverity::Critical => FindingSeverity::High,
467                FindingSeverity::High => {
468                    if self.rng.random::<bool>() {
469                        FindingSeverity::Critical
470                    } else {
471                        FindingSeverity::Medium
472                    }
473                }
474                FindingSeverity::Medium => {
475                    if self.rng.random::<bool>() {
476                        FindingSeverity::High
477                    } else {
478                        FindingSeverity::Low
479                    }
480                }
481                FindingSeverity::Low => FindingSeverity::Medium,
482                FindingSeverity::Informational => FindingSeverity::Low,
483            }
484        } else {
485            base_severity
486        }
487    }
488
489    /// Check if finding type is a misstatement.
490    fn is_misstatement_type(&self, finding_type: FindingType) -> bool {
491        matches!(
492            finding_type,
493            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement
494        )
495    }
496
497    /// Generate misstatement amounts.
498    fn generate_misstatement_amounts(
499        &mut self,
500    ) -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
501        let factual = Decimal::new(
502            self.rng
503                .random_range(self.config.misstatement_range.0..self.config.misstatement_range.1),
504            0,
505        );
506
507        let projected = if self.rng.random::<f64>() < 0.5 {
508            Some(Decimal::new(
509                self.rng
510                    .random_range(0..self.config.misstatement_range.1 / 2),
511                0,
512            ))
513        } else {
514            None
515        };
516
517        let judgmental = if self.rng.random::<f64>() < 0.3 {
518            Some(Decimal::new(
519                self.rng
520                    .random_range(0..self.config.misstatement_range.1 / 4),
521                0,
522            ))
523        } else {
524            None
525        };
526
527        (Some(factual), projected, judgmental)
528    }
529
530    /// Select assertions affected.
531    fn select_assertions(&mut self, finding_type: FindingType) -> Vec<Assertion> {
532        let mut assertions = Vec::new();
533
534        match finding_type {
535            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
536                assertions.push(Assertion::Accuracy);
537                if self.rng.random::<bool>() {
538                    assertions.push(Assertion::ValuationAndAllocation);
539                }
540            }
541            FindingType::MaterialWeakness
542            | FindingType::SignificantDeficiency
543            | FindingType::ControlDeficiency => {
544                let possible = [
545                    Assertion::Occurrence,
546                    Assertion::Completeness,
547                    Assertion::Accuracy,
548                    Assertion::Classification,
549                ];
550                let count = self.rng.random_range(1..=3);
551                for _ in 0..count {
552                    let idx = self.rng.random_range(0..possible.len());
553                    if !assertions.contains(&possible[idx]) {
554                        assertions.push(possible[idx]);
555                    }
556                }
557            }
558            _ => {
559                assertions.push(Assertion::PresentationAndDisclosure);
560            }
561        }
562
563        assertions
564    }
565
566    /// Select process areas.
567    fn select_process_areas(&mut self, account: &str) -> Vec<String> {
568        let account_lower = account.to_lowercase();
569
570        if account_lower.contains("revenue") || account_lower.contains("receivable") {
571            vec!["Order to Cash".into(), "Revenue Recognition".into()]
572        } else if account_lower.contains("payable") || account_lower.contains("expense") {
573            vec!["Procure to Pay".into(), "Expense Management".into()]
574        } else if account_lower.contains("inventory") {
575            vec!["Inventory Management".into(), "Cost of Goods Sold".into()]
576        } else if account_lower.contains("fixed asset") {
577            vec!["Capital Asset Management".into()]
578        } else if account_lower.contains("it") {
579            vec![
580                "IT General Controls".into(),
581                "IT Application Controls".into(),
582            ]
583        } else if account_lower.contains("payroll") {
584            vec!["Hire to Retire".into(), "Payroll Processing".into()]
585        } else {
586            vec!["Financial Close".into()]
587        }
588    }
589
590    /// Generate management response.
591    fn generate_management_response(&mut self, finding_type: FindingType, agrees: bool) -> String {
592        if agrees {
593            match finding_type {
594                FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
595                    "Management agrees with the finding and has initiated a remediation plan to \
596                    address the identified control deficiency. We expect to complete remediation \
597                    prior to the next audit cycle."
598                        .into()
599                }
600                FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
601                    "Management agrees with the proposed adjustment and will record the entry. \
602                    We have implemented additional review procedures to prevent similar errors."
603                        .into()
604                }
605                _ => "Management agrees with the observation and will implement the recommended \
606                    improvements as resources permit."
607                    .into(),
608            }
609        } else {
610            "Management respectfully disagrees with the finding. We believe that existing \
611            controls are adequate and operating effectively. We will provide additional \
612            documentation to support our position."
613                .into()
614        }
615    }
616
617    /// Generate remediation plan.
618    fn generate_remediation_plan(
619        &mut self,
620        finding: &AuditFinding,
621        account: &str,
622    ) -> RemediationPlan {
623        let target_date = finding.identified_date + Duration::days(self.rng.random_range(60..180));
624
625        let description = format!(
626            "Implement enhanced controls and monitoring procedures for {account} to address \
627            the identified deficiency. This includes updated policies, additional training, \
628            and implementation of automated controls where feasible."
629        );
630
631        let responsible_party = format!(
632            "{} Manager",
633            if account.to_lowercase().contains("it") {
634                "IT"
635            } else {
636                "Controller"
637            }
638        );
639
640        let mut plan = RemediationPlan::new(
641            finding.finding_id,
642            &description,
643            &responsible_party,
644            target_date,
645        );
646
647        plan.validation_approach =
648            "Auditor will test remediated controls during the next audit cycle.".into();
649
650        // Add milestones
651        let milestone_dates = [
652            (
653                finding.identified_date + Duration::days(30),
654                "Complete root cause analysis",
655            ),
656            (
657                finding.identified_date + Duration::days(60),
658                "Document updated control procedures",
659            ),
660            (
661                finding.identified_date + Duration::days(90),
662                "Implement control changes",
663            ),
664            (target_date, "Complete testing and validation"),
665        ];
666
667        for (date, desc) in milestone_dates {
668            plan.add_milestone(desc, date);
669        }
670
671        // Maybe mark some progress
672        if self.rng.random::<f64>() < 0.3 {
673            plan.status = RemediationStatus::InProgress;
674            if !plan.milestones.is_empty() {
675                plan.milestones[0].status = MilestoneStatus::Complete;
676                plan.milestones[0].completion_date = Some(plan.milestones[0].target_date);
677            }
678        }
679
680        plan
681    }
682
683    /// Select a team member.
684    fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
685        let matching: Vec<&String> = team_members
686            .iter()
687            .filter(|m| m.to_lowercase().contains(role_hint))
688            .collect();
689
690        if let Some(&member) = matching.first() {
691            member.clone()
692        } else if !team_members.is_empty() {
693            let idx = self.rng.random_range(0..team_members.len());
694            team_members[idx].clone()
695        } else {
696            format!("{}001", role_hint.to_uppercase())
697        }
698    }
699}
700
701#[cfg(test)]
702#[allow(clippy::unwrap_used)]
703mod tests {
704    use super::*;
705    use crate::audit::test_helpers::create_test_engagement;
706
707    #[test]
708    fn test_finding_generation() {
709        let mut generator = FindingGenerator::new(42);
710        let engagement = create_test_engagement();
711        let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
712
713        let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
714
715        assert!(!findings.is_empty());
716        for finding in &findings {
717            assert!(!finding.condition.is_empty());
718            assert!(!finding.criteria.is_empty());
719            assert!(!finding.recommendation.is_empty());
720        }
721    }
722
723    #[test]
724    fn test_finding_types_distribution() {
725        let mut generator = FindingGenerator::new(42);
726        let engagement = create_test_engagement();
727        let team = vec!["STAFF001".into()];
728
729        // Generate many findings to check distribution
730        let config = FindingGeneratorConfig {
731            findings_per_engagement: (50, 50),
732            ..Default::default()
733        };
734        generator.config = config;
735
736        let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
737
738        let material_weaknesses = findings
739            .iter()
740            .filter(|f| f.finding_type == FindingType::MaterialWeakness)
741            .count();
742        let significant_deficiencies = findings
743            .iter()
744            .filter(|f| f.finding_type == FindingType::SignificantDeficiency)
745            .count();
746
747        // Material weaknesses should be rare
748        assert!(material_weaknesses < 10);
749        // Significant deficiencies should be more common than material weaknesses
750        assert!(significant_deficiencies > material_weaknesses);
751    }
752
753    #[test]
754    fn test_misstatement_finding() {
755        let config = FindingGeneratorConfig {
756            misstatement_probability: 1.0,
757            material_weakness_probability: 0.0,
758            significant_deficiency_probability: 0.0,
759            ..Default::default()
760        };
761        let mut generator = FindingGenerator::with_config(42, config);
762        let engagement = create_test_engagement();
763
764        let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
765
766        assert!(finding.is_misstatement);
767        assert!(finding.factual_misstatement.is_some() || finding.projected_misstatement.is_some());
768    }
769
770    #[test]
771    fn test_remediation_plan() {
772        let config = FindingGeneratorConfig {
773            remediation_plan_probability: 1.0,
774            management_agrees_probability: 1.0,
775            ..Default::default()
776        };
777        let mut generator = FindingGenerator::with_config(42, config);
778        let engagement = create_test_engagement();
779
780        let findings =
781            generator.generate_findings_for_engagement(&engagement, &[], &["STAFF001".into()]);
782
783        // At least some findings should have remediation plans
784        let with_plans = findings
785            .iter()
786            .filter(|f| f.remediation_plan.is_some())
787            .count();
788        assert!(with_plans > 0);
789
790        for finding in findings.iter().filter(|f| f.remediation_plan.is_some()) {
791            let plan = finding.remediation_plan.as_ref().unwrap();
792            assert!(!plan.description.is_empty());
793            assert!(!plan.milestones.is_empty());
794        }
795    }
796
797    #[test]
798    fn test_governance_communication() {
799        let config = FindingGeneratorConfig {
800            material_weakness_probability: 1.0,
801            ..Default::default()
802        };
803        let mut generator = FindingGenerator::with_config(42, config);
804        let engagement = create_test_engagement();
805
806        let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
807
808        assert!(finding.report_to_governance);
809        assert!(finding.include_in_management_letter);
810    }
811}