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