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