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 {} transactions receive appropriate \
353                    review and approval prior to processing.",
354                    account
355                );
356                let cause = "Staffing constraints and competing priorities resulted in reduced focus on control execution.".into();
357                let effect = format!(
358                    "Transactions may be processed without appropriate oversight, increasing the risk of errors \
359                    or fraud in the {} balance.",
360                    account
361                );
362                (condition, criteria, cause, effect)
363            }
364            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
365                let amount = self.rng.random_range(
366                    self.config.misstatement_range.0..self.config.misstatement_range.1,
367                );
368                let condition = format!(
369                    "Our testing identified a misstatement in {} of approximately ${}. \
370                    The error resulted from incorrect application of accounting standards.",
371                    account, amount
372                );
373                let criteria = "US GAAP and company accounting policy require accurate recording of all transactions.".into();
374                let cause =
375                    "Manual calculation error combined with inadequate review procedures.".into();
376                let effect = format!(
377                    "The {} balance was {} by ${}, which {}.",
378                    account,
379                    if self.rng.random::<bool>() {
380                        "overstated"
381                    } else {
382                        "understated"
383                    },
384                    amount,
385                    if finding_type == FindingType::MaterialMisstatement {
386                        "represents a material misstatement"
387                    } else {
388                        "is below materiality but has been communicated to management"
389                    }
390                );
391                (condition, criteria, cause, effect)
392            }
393            FindingType::ComplianceException => {
394                let condition = format!(
395                    "The Company did not comply with {} regulatory requirements during the period under audit.",
396                    account
397                );
398                let criteria =
399                    "Applicable laws and regulations require timely and accurate compliance."
400                        .into();
401                let cause = "Lack of monitoring procedures to track compliance deadlines.".into();
402                let effect =
403                    "The Company may be subject to penalties or regulatory scrutiny.".into();
404                (condition, criteria, cause, effect)
405            }
406            _ => {
407                let condition = format!(
408                    "We identified an opportunity to enhance the {} process.",
409                    account
410                );
411                let criteria =
412                    "Industry best practices suggest continuous improvement in control processes."
413                        .into();
414                let cause =
415                    "Current processes have not been updated to reflect operational changes."
416                        .into();
417                let effect =
418                    "Operational efficiency could be improved with process enhancements.".into();
419                (condition, criteria, cause, effect)
420            }
421        }
422    }
423
424    /// Generate recommendation.
425    fn generate_recommendation(&mut self, finding_type: FindingType, account: &str) -> String {
426        match finding_type {
427            FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
428                format!(
429                    "We recommend that management: (1) Implement additional review procedures for {} transactions, \
430                    (2) Document all control activities contemporaneously, and \
431                    (3) Provide additional training to personnel responsible for control execution.",
432                    account
433                )
434            }
435            FindingType::ControlDeficiency => {
436                format!(
437                    "We recommend that management strengthen the {} control by ensuring timely execution \
438                    and documentation of all required review activities.",
439                    account
440                )
441            }
442            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
443                "We recommend that management record the proposed adjusting entry and implement \
444                additional review procedures to prevent similar errors in future periods.".into()
445            }
446            FindingType::ComplianceException => {
447                "We recommend that management implement a compliance calendar with automated reminders \
448                and establish monitoring procedures to ensure timely compliance.".into()
449            }
450            FindingType::ItDeficiency => {
451                "We recommend that IT management review and remediate the identified access control \
452                weaknesses and implement periodic access certification procedures.".into()
453            }
454            _ => {
455                format!(
456                    "We recommend that management evaluate the {} process for potential \
457                    efficiency improvements and implement changes as appropriate.",
458                    account
459                )
460            }
461        }
462    }
463
464    /// Determine severity based on finding type and other factors.
465    fn determine_severity(
466        &mut self,
467        finding_type: FindingType,
468        _finding: &AuditFinding,
469    ) -> FindingSeverity {
470        let base_severity = finding_type.default_severity();
471
472        // Maybe adjust severity
473        if self.rng.random::<f64>() < 0.2 {
474            match base_severity {
475                FindingSeverity::Critical => FindingSeverity::High,
476                FindingSeverity::High => {
477                    if self.rng.random::<bool>() {
478                        FindingSeverity::Critical
479                    } else {
480                        FindingSeverity::Medium
481                    }
482                }
483                FindingSeverity::Medium => {
484                    if self.rng.random::<bool>() {
485                        FindingSeverity::High
486                    } else {
487                        FindingSeverity::Low
488                    }
489                }
490                FindingSeverity::Low => FindingSeverity::Medium,
491                FindingSeverity::Informational => FindingSeverity::Low,
492            }
493        } else {
494            base_severity
495        }
496    }
497
498    /// Check if finding type is a misstatement.
499    fn is_misstatement_type(&self, finding_type: FindingType) -> bool {
500        matches!(
501            finding_type,
502            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement
503        )
504    }
505
506    /// Generate misstatement amounts.
507    fn generate_misstatement_amounts(
508        &mut self,
509    ) -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
510        let factual = Decimal::new(
511            self.rng
512                .random_range(self.config.misstatement_range.0..self.config.misstatement_range.1),
513            0,
514        );
515
516        let projected = if self.rng.random::<f64>() < 0.5 {
517            Some(Decimal::new(
518                self.rng
519                    .random_range(0..self.config.misstatement_range.1 / 2),
520                0,
521            ))
522        } else {
523            None
524        };
525
526        let judgmental = if self.rng.random::<f64>() < 0.3 {
527            Some(Decimal::new(
528                self.rng
529                    .random_range(0..self.config.misstatement_range.1 / 4),
530                0,
531            ))
532        } else {
533            None
534        };
535
536        (Some(factual), projected, judgmental)
537    }
538
539    /// Select assertions affected.
540    fn select_assertions(&mut self, finding_type: FindingType) -> Vec<Assertion> {
541        let mut assertions = Vec::new();
542
543        match finding_type {
544            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
545                assertions.push(Assertion::Accuracy);
546                if self.rng.random::<bool>() {
547                    assertions.push(Assertion::ValuationAndAllocation);
548                }
549            }
550            FindingType::MaterialWeakness
551            | FindingType::SignificantDeficiency
552            | FindingType::ControlDeficiency => {
553                let possible = [
554                    Assertion::Occurrence,
555                    Assertion::Completeness,
556                    Assertion::Accuracy,
557                    Assertion::Classification,
558                ];
559                let count = self.rng.random_range(1..=3);
560                for _ in 0..count {
561                    let idx = self.rng.random_range(0..possible.len());
562                    if !assertions.contains(&possible[idx]) {
563                        assertions.push(possible[idx]);
564                    }
565                }
566            }
567            _ => {
568                assertions.push(Assertion::PresentationAndDisclosure);
569            }
570        }
571
572        assertions
573    }
574
575    /// Select process areas.
576    fn select_process_areas(&mut self, account: &str) -> Vec<String> {
577        let account_lower = account.to_lowercase();
578
579        if account_lower.contains("revenue") || account_lower.contains("receivable") {
580            vec!["Order to Cash".into(), "Revenue Recognition".into()]
581        } else if account_lower.contains("payable") || account_lower.contains("expense") {
582            vec!["Procure to Pay".into(), "Expense Management".into()]
583        } else if account_lower.contains("inventory") {
584            vec!["Inventory Management".into(), "Cost of Goods Sold".into()]
585        } else if account_lower.contains("fixed asset") {
586            vec!["Capital Asset Management".into()]
587        } else if account_lower.contains("it") {
588            vec![
589                "IT General Controls".into(),
590                "IT Application Controls".into(),
591            ]
592        } else if account_lower.contains("payroll") {
593            vec!["Hire to Retire".into(), "Payroll Processing".into()]
594        } else {
595            vec!["Financial Close".into()]
596        }
597    }
598
599    /// Generate management response.
600    fn generate_management_response(&mut self, finding_type: FindingType, agrees: bool) -> String {
601        if agrees {
602            match finding_type {
603                FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
604                    "Management agrees with the finding and has initiated a remediation plan to \
605                    address the identified control deficiency. We expect to complete remediation \
606                    prior to the next audit cycle."
607                        .into()
608                }
609                FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
610                    "Management agrees with the proposed adjustment and will record the entry. \
611                    We have implemented additional review procedures to prevent similar errors."
612                        .into()
613                }
614                _ => "Management agrees with the observation and will implement the recommended \
615                    improvements as resources permit."
616                    .into(),
617            }
618        } else {
619            "Management respectfully disagrees with the finding. We believe that existing \
620            controls are adequate and operating effectively. We will provide additional \
621            documentation to support our position."
622                .into()
623        }
624    }
625
626    /// Generate remediation plan.
627    fn generate_remediation_plan(
628        &mut self,
629        finding: &AuditFinding,
630        account: &str,
631    ) -> RemediationPlan {
632        let target_date = finding.identified_date + Duration::days(self.rng.random_range(60..180));
633
634        let description = format!(
635            "Implement enhanced controls and monitoring procedures for {} to address \
636            the identified deficiency. This includes updated policies, additional training, \
637            and implementation of automated controls where feasible.",
638            account
639        );
640
641        let responsible_party = format!(
642            "{} Manager",
643            if account.to_lowercase().contains("it") {
644                "IT"
645            } else {
646                "Controller"
647            }
648        );
649
650        let mut plan = RemediationPlan::new(
651            finding.finding_id,
652            &description,
653            &responsible_party,
654            target_date,
655        );
656
657        plan.validation_approach =
658            "Auditor will test remediated controls during the next audit cycle.".into();
659
660        // Add milestones
661        let milestone_dates = [
662            (
663                finding.identified_date + Duration::days(30),
664                "Complete root cause analysis",
665            ),
666            (
667                finding.identified_date + Duration::days(60),
668                "Document updated control procedures",
669            ),
670            (
671                finding.identified_date + Duration::days(90),
672                "Implement control changes",
673            ),
674            (target_date, "Complete testing and validation"),
675        ];
676
677        for (date, desc) in milestone_dates {
678            plan.add_milestone(desc, date);
679        }
680
681        // Maybe mark some progress
682        if self.rng.random::<f64>() < 0.3 {
683            plan.status = RemediationStatus::InProgress;
684            if !plan.milestones.is_empty() {
685                plan.milestones[0].status = MilestoneStatus::Complete;
686                plan.milestones[0].completion_date = Some(plan.milestones[0].target_date);
687            }
688        }
689
690        plan
691    }
692
693    /// Select a team member.
694    fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
695        let matching: Vec<&String> = team_members
696            .iter()
697            .filter(|m| m.to_lowercase().contains(role_hint))
698            .collect();
699
700        if let Some(&member) = matching.first() {
701            member.clone()
702        } else if !team_members.is_empty() {
703            let idx = self.rng.random_range(0..team_members.len());
704            team_members[idx].clone()
705        } else {
706            format!("{}001", role_hint.to_uppercase())
707        }
708    }
709}
710
711#[cfg(test)]
712#[allow(clippy::unwrap_used)]
713mod tests {
714    use super::*;
715    use crate::audit::test_helpers::create_test_engagement;
716
717    #[test]
718    fn test_finding_generation() {
719        let mut generator = FindingGenerator::new(42);
720        let engagement = create_test_engagement();
721        let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
722
723        let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
724
725        assert!(!findings.is_empty());
726        for finding in &findings {
727            assert!(!finding.condition.is_empty());
728            assert!(!finding.criteria.is_empty());
729            assert!(!finding.recommendation.is_empty());
730        }
731    }
732
733    #[test]
734    fn test_finding_types_distribution() {
735        let mut generator = FindingGenerator::new(42);
736        let engagement = create_test_engagement();
737        let team = vec!["STAFF001".into()];
738
739        // Generate many findings to check distribution
740        let config = FindingGeneratorConfig {
741            findings_per_engagement: (50, 50),
742            ..Default::default()
743        };
744        generator.config = config;
745
746        let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
747
748        let material_weaknesses = findings
749            .iter()
750            .filter(|f| f.finding_type == FindingType::MaterialWeakness)
751            .count();
752        let significant_deficiencies = findings
753            .iter()
754            .filter(|f| f.finding_type == FindingType::SignificantDeficiency)
755            .count();
756
757        // Material weaknesses should be rare
758        assert!(material_weaknesses < 10);
759        // Significant deficiencies should be more common than material weaknesses
760        assert!(significant_deficiencies > material_weaknesses);
761    }
762
763    #[test]
764    fn test_misstatement_finding() {
765        let config = FindingGeneratorConfig {
766            misstatement_probability: 1.0,
767            material_weakness_probability: 0.0,
768            significant_deficiency_probability: 0.0,
769            ..Default::default()
770        };
771        let mut generator = FindingGenerator::with_config(42, config);
772        let engagement = create_test_engagement();
773
774        let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
775
776        assert!(finding.is_misstatement);
777        assert!(finding.factual_misstatement.is_some() || finding.projected_misstatement.is_some());
778    }
779
780    #[test]
781    fn test_remediation_plan() {
782        let config = FindingGeneratorConfig {
783            remediation_plan_probability: 1.0,
784            management_agrees_probability: 1.0,
785            ..Default::default()
786        };
787        let mut generator = FindingGenerator::with_config(42, config);
788        let engagement = create_test_engagement();
789
790        let findings =
791            generator.generate_findings_for_engagement(&engagement, &[], &["STAFF001".into()]);
792
793        // At least some findings should have remediation plans
794        let with_plans = findings
795            .iter()
796            .filter(|f| f.remediation_plan.is_some())
797            .count();
798        assert!(with_plans > 0);
799
800        for finding in findings.iter().filter(|f| f.remediation_plan.is_some()) {
801            let plan = finding.remediation_plan.as_ref().unwrap();
802            assert!(!plan.description.is_empty());
803            assert!(!plan.milestones.is_empty());
804        }
805    }
806
807    #[test]
808    fn test_governance_communication() {
809        let config = FindingGeneratorConfig {
810            material_weakness_probability: 1.0,
811            ..Default::default()
812        };
813        let mut generator = FindingGenerator::with_config(42, config);
814        let engagement = create_test_engagement();
815
816        let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
817
818        assert!(finding.report_to_governance);
819        assert!(finding.include_in_management_letter);
820    }
821}