Skip to main content

simular/edd/
audit.rs

1//! Turn-by-Turn Audit Logging for EDD Simulations.
2//!
3//! **HARD REQUIREMENT (EDD-16, EDD-17, EDD-18):** Every simulation MUST produce
4//! a complete audit trail of every step.
5//!
6//! This module provides:
7//! - `SimulationAuditLog` trait for mandatory audit logging
8//! - `StepEntry` for capturing complete step state
9//! - `EquationEval` for logging equation computations
10//! - `Decision` for logging algorithmic choices
11//! - Automatic test case generation from logs
12//! - Replay functionality with speed control
13//!
14//! # The Provability Chain
15//!
16//! ```text
17//! Seed(42) → RNG State₀ → Decision₁ → RNG State₁ → Decision₂ → ... → Final
18//!     ↓           ↓            ↓           ↓            ↓
19//!   KNOWN     PROVABLE     PROVABLE    PROVABLE     PROVABLE
20//! ```
21//!
22//! Every step is deterministic given the seed, therefore every step is provable.
23//!
24//! # References
25//!
26//! - EDD Spec Section 1.6: Turn-by-Turn Audit Logging (MANDATORY)
27//! - Quality Gates: EDD-16, EDD-17, EDD-18
28
29use indexmap::IndexMap;
30use serde::{Deserialize, Serialize};
31use std::time::Duration;
32
33use crate::engine::SimTime;
34use crate::error::{SimError, SimResult};
35
36// =============================================================================
37// Core Audit Types (EDD-16)
38// =============================================================================
39
40/// Equation evaluation record (EDD-17).
41///
42/// Every equation evaluation MUST be logged with inputs, result, and optional
43/// expected value for verification.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub struct EquationEval {
46    /// Equation identifier from EMC (e.g., `two_opt_delta`, `tour_length`)
47    pub equation_id: String,
48
49    /// Input values with variable names
50    pub inputs: IndexMap<String, f64>,
51
52    /// Computed result
53    pub result: f64,
54
55    /// Expected result (if known analytically)
56    pub expected: Option<f64>,
57
58    /// Absolute error (|result - expected| if expected is known)
59    pub error: Option<f64>,
60
61    /// Whether Z3 verified this computation
62    #[serde(default)]
63    pub z3_verified: Option<bool>,
64}
65
66impl EquationEval {
67    /// Create a new equation evaluation record.
68    #[must_use]
69    pub fn new(equation_id: impl Into<String>, result: f64) -> Self {
70        Self {
71            equation_id: equation_id.into(),
72            inputs: IndexMap::new(),
73            result,
74            expected: None,
75            error: None,
76            z3_verified: None,
77        }
78    }
79
80    /// Add an input variable.
81    #[must_use]
82    pub fn with_input(mut self, name: impl Into<String>, value: f64) -> Self {
83        self.inputs.insert(name.into(), value);
84        self
85    }
86
87    /// Set expected value and compute error.
88    #[must_use]
89    pub fn with_expected(mut self, expected: f64) -> Self {
90        self.expected = Some(expected);
91        self.error = Some((self.result - expected).abs());
92        self
93    }
94
95    /// Mark as Z3 verified.
96    #[must_use]
97    pub fn with_z3_verified(mut self, verified: bool) -> Self {
98        self.z3_verified = Some(verified);
99        self
100    }
101
102    /// Check if result matches expected within tolerance.
103    #[must_use]
104    pub fn is_correct(&self, tolerance: f64) -> bool {
105        self.error.is_none_or(|err| err <= tolerance)
106    }
107}
108
109/// Algorithmic decision record.
110///
111/// Every decision MUST be logged with options considered, choice made,
112/// and the rationale (metrics that drove the choice).
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114pub struct Decision {
115    /// Decision type (e.g., `rcl_selection`, `two_opt_apply`, `update_best`)
116    pub decision_type: String,
117
118    /// Options that were considered
119    pub options: Vec<String>,
120
121    /// The option that was chosen
122    pub chosen: String,
123
124    /// Rationale: metrics that drove the choice
125    pub rationale: IndexMap<String, f64>,
126
127    /// RNG value used (if decision involved randomness)
128    pub rng_value: Option<f64>,
129}
130
131impl Decision {
132    /// Create a new decision record.
133    #[must_use]
134    pub fn new(decision_type: impl Into<String>, chosen: impl Into<String>) -> Self {
135        Self {
136            decision_type: decision_type.into(),
137            options: Vec::new(),
138            chosen: chosen.into(),
139            rationale: IndexMap::new(),
140            rng_value: None,
141        }
142    }
143
144    /// Add options that were considered.
145    #[must_use]
146    pub fn with_options(mut self, options: Vec<String>) -> Self {
147        self.options = options;
148        self
149    }
150
151    /// Add a rationale metric.
152    #[must_use]
153    pub fn with_rationale(mut self, name: impl Into<String>, value: f64) -> Self {
154        self.rationale.insert(name.into(), value);
155        self
156    }
157
158    /// Set the RNG value used.
159    #[must_use]
160    pub fn with_rng_value(mut self, value: f64) -> Self {
161        self.rng_value = Some(value);
162        self
163    }
164}
165
166/// Complete step entry for audit logging (EDD-16).
167///
168/// Every step MUST include all required fields for full reproducibility
169/// and verification.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct StepEntry<S: Clone> {
172    /// Monotonic step counter
173    pub step_id: u64,
174
175    /// Simulation time at this step
176    pub timestamp: SimTime,
177
178    /// Blake3 hash of RNG state BEFORE this step
179    pub rng_state_before: [u8; 32],
180
181    /// Blake3 hash of RNG state AFTER this step
182    pub rng_state_after: [u8; 32],
183
184    /// State snapshot before step (for replay verification)
185    pub input_state: S,
186
187    /// State snapshot after step
188    pub output_state: S,
189
190    /// All equation evaluations performed in this step
191    pub equation_evaluations: Vec<EquationEval>,
192
193    /// All decisions made in this step
194    pub decisions: Vec<Decision>,
195
196    /// Step type identifier (simulation-specific)
197    pub step_type: String,
198
199    /// Duration of step computation (for profiling)
200    pub compute_duration_us: u64,
201}
202
203impl<S: Clone + Serialize> StepEntry<S> {
204    /// Create a new step entry.
205    #[must_use]
206    pub fn new(
207        step_id: u64,
208        timestamp: SimTime,
209        step_type: impl Into<String>,
210        input_state: S,
211        output_state: S,
212    ) -> Self {
213        contract_pre_iterator!();
214        contract_post_configuration!(&"ok");
215        Self {
216            step_id,
217            timestamp,
218            rng_state_before: [0; 32],
219            rng_state_after: [0; 32],
220            input_state,
221            output_state,
222            equation_evaluations: Vec::new(),
223            decisions: Vec::new(),
224            step_type: step_type.into(),
225            compute_duration_us: 0,
226        }
227    }
228
229    /// Set RNG state hashes.
230    #[must_use]
231    pub fn with_rng_states(mut self, before: [u8; 32], after: [u8; 32]) -> Self {
232        self.rng_state_before = before;
233        self.rng_state_after = after;
234        self
235    }
236
237    /// Add an equation evaluation.
238    pub fn add_equation_eval(&mut self, eval: EquationEval) {
239        self.equation_evaluations.push(eval);
240    }
241
242    /// Add a decision.
243    pub fn add_decision(&mut self, decision: Decision) {
244        self.decisions.push(decision);
245    }
246
247    /// Set compute duration.
248    #[must_use]
249    pub fn with_duration(mut self, duration_us: u64) -> Self {
250        self.compute_duration_us = duration_us;
251        self
252    }
253}
254
255// =============================================================================
256// Generated Test Case (EDD-18)
257// =============================================================================
258
259/// Test case generated from audit log.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct GeneratedTestCase {
262    /// Test case name
263    pub name: String,
264
265    /// Equation being tested
266    pub equation_id: String,
267
268    /// Input values
269    pub inputs: IndexMap<String, f64>,
270
271    /// Expected output
272    pub expected_output: f64,
273
274    /// Assertion description
275    pub assertion: String,
276
277    /// Source step ID
278    pub source_step_id: u64,
279
280    /// Tolerance for floating-point comparison
281    pub tolerance: f64,
282}
283
284impl GeneratedTestCase {
285    /// Create a new generated test case.
286    #[must_use]
287    pub fn new(
288        name: impl Into<String>,
289        equation_id: impl Into<String>,
290        expected_output: f64,
291        source_step_id: u64,
292    ) -> Self {
293        Self {
294            name: name.into(),
295            equation_id: equation_id.into(),
296            inputs: IndexMap::new(),
297            expected_output,
298            assertion: String::new(),
299            source_step_id,
300            tolerance: 1e-10,
301        }
302    }
303
304    /// Add input.
305    #[must_use]
306    pub fn with_input(mut self, name: impl Into<String>, value: f64) -> Self {
307        self.inputs.insert(name.into(), value);
308        self
309    }
310
311    /// Set assertion.
312    #[must_use]
313    pub fn with_assertion(mut self, assertion: impl Into<String>) -> Self {
314        self.assertion = assertion.into();
315        self
316    }
317
318    /// Set tolerance.
319    #[must_use]
320    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
321        self.tolerance = tolerance;
322        self
323    }
324
325    /// Generate Rust test code.
326    #[must_use]
327    pub fn to_rust_test(&self) -> String {
328        let inputs_str: Vec<String> = self
329            .inputs
330            .iter()
331            .map(|(k, v)| format!("    let {k} = {v}_f64;"))
332            .collect();
333
334        format!(
335            r#"#[test]
336fn {name}() {{
337    // Generated from step {step_id}
338    // Equation: {equation_id}
339{inputs}
340    let result = /* compute {equation_id} */;
341    let expected = {expected}_f64;
342    assert!(
343        (result - expected).abs() < {tolerance},
344        "{assertion}: got {{result}}, expected {{expected}}"
345    );
346}}
347"#,
348            name = self.name,
349            step_id = self.source_step_id,
350            equation_id = self.equation_id,
351            inputs = inputs_str.join("\n"),
352            expected = self.expected_output,
353            tolerance = self.tolerance,
354            assertion = self.assertion,
355        )
356    }
357}
358
359// =============================================================================
360// Audit Log Trait (MANDATORY)
361// =============================================================================
362
363/// MANDATORY trait for all EDD simulations (EDD-16, EDD-17, EDD-18).
364///
365/// Every simulation MUST implement this trait to produce a complete
366/// audit trail of every step.
367pub trait SimulationAuditLog {
368    /// State snapshot type (must be serializable and cloneable)
369    type StateSnapshot: Clone + Serialize + for<'de> Deserialize<'de>;
370
371    /// Record a step with full state capture.
372    fn log_step(&mut self, entry: StepEntry<Self::StateSnapshot>);
373
374    /// Get all logged entries.
375    fn audit_log(&self) -> &[StepEntry<Self::StateSnapshot>];
376
377    /// Get mutable access to audit log.
378    fn audit_log_mut(&mut self) -> &mut Vec<StepEntry<Self::StateSnapshot>>;
379
380    /// Clear the audit log.
381    fn clear_audit_log(&mut self);
382
383    /// Export log as JSON for analysis.
384    ///
385    /// # Errors
386    ///
387    /// Returns error if serialization fails.
388    fn export_audit_json(&self) -> SimResult<String> {
389        serde_json::to_string_pretty(self.audit_log())
390            .map_err(|e| SimError::serialization(format!("Audit log JSON export: {e}")))
391    }
392
393    /// Export log as compact JSON (no pretty printing).
394    ///
395    /// # Errors
396    ///
397    /// Returns error if serialization fails.
398    fn export_audit_json_compact(&self) -> SimResult<String> {
399        serde_json::to_string(self.audit_log())
400            .map_err(|e| SimError::serialization(format!("Audit log JSON export: {e}")))
401    }
402
403    /// Generate test cases from audit log (EDD-18).
404    fn generate_test_cases(&self) -> Vec<GeneratedTestCase> {
405        let mut test_cases = Vec::new();
406
407        for entry in self.audit_log() {
408            for eval in &entry.equation_evaluations {
409                let mut tc = GeneratedTestCase::new(
410                    format!("test_{}_{}", eval.equation_id, entry.step_id),
411                    &eval.equation_id,
412                    eval.result,
413                    entry.step_id,
414                );
415
416                for (name, value) in &eval.inputs {
417                    tc = tc.with_input(name, *value);
418                }
419
420                if let Some(expected) = eval.expected {
421                    tc = tc.with_assertion(format!(
422                        "{} should equal {expected} (computed {result})",
423                        eval.equation_id,
424                        result = eval.result
425                    ));
426                } else {
427                    tc = tc.with_assertion(format!("{} computation", eval.equation_id));
428                }
429
430                test_cases.push(tc);
431            }
432        }
433
434        test_cases
435    }
436
437    /// Get total number of equation evaluations in log.
438    fn total_equation_evals(&self) -> usize {
439        self.audit_log()
440            .iter()
441            .map(|e| e.equation_evaluations.len())
442            .sum()
443    }
444
445    /// Get total number of decisions in log.
446    fn total_decisions(&self) -> usize {
447        self.audit_log().iter().map(|e| e.decisions.len()).sum()
448    }
449
450    /// Verify all equation evaluations are correct within tolerance.
451    fn verify_all_equations(&self, tolerance: f64) -> Vec<(u64, String, f64)> {
452        let mut failures = Vec::new();
453
454        for entry in self.audit_log() {
455            for eval in &entry.equation_evaluations {
456                if !eval.is_correct(tolerance) {
457                    if let Some(err) = eval.error {
458                        failures.push((entry.step_id, eval.equation_id.clone(), err));
459                    }
460                }
461            }
462        }
463
464        failures
465    }
466}
467
468// =============================================================================
469// Replay Support
470// =============================================================================
471
472/// Replay speed control.
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
474pub enum ReplaySpeed {
475    /// Instant replay (no delays)
476    #[default]
477    Instant,
478    /// Real-time replay with specified delay per step
479    RealTime(Duration),
480    /// Step-by-step (wait for external trigger)
481    StepByStep,
482    /// Fast forward (N times real speed)
483    FastForward(u32),
484}
485
486/// Replay state for audit log playback.
487#[derive(Debug, Clone)]
488pub struct ReplayState<S: Clone> {
489    /// Current step index in the log
490    pub current_index: usize,
491    /// Total steps in the log
492    pub total_steps: usize,
493    /// Current state snapshot
494    pub current_state: Option<S>,
495    /// Replay speed
496    pub speed: ReplaySpeed,
497    /// Whether replay is paused
498    pub paused: bool,
499}
500
501impl<S: Clone> ReplayState<S> {
502    /// Create a new replay state.
503    #[must_use]
504    pub fn new(total_steps: usize) -> Self {
505        Self {
506            current_index: 0,
507            total_steps,
508            current_state: None,
509            speed: ReplaySpeed::default(),
510            paused: false,
511        }
512    }
513
514    /// Check if replay is complete.
515    #[must_use]
516    pub fn is_complete(&self) -> bool {
517        self.current_index >= self.total_steps
518    }
519
520    /// Get progress as percentage.
521    #[must_use]
522    pub fn progress_percent(&self) -> f64 {
523        if self.total_steps == 0 {
524            100.0
525        } else {
526            (self.current_index as f64 / self.total_steps as f64) * 100.0
527        }
528    }
529
530    /// Advance to next step.
531    pub fn advance(&mut self) {
532        if self.current_index < self.total_steps {
533            self.current_index += 1;
534        }
535    }
536
537    /// Seek to specific step.
538    pub fn seek(&mut self, step_index: usize) {
539        contract_pre_iterator!();
540        self.current_index = step_index.min(self.total_steps);
541    }
542
543    /// Reset to beginning.
544    pub fn reset(&mut self) {
545        self.current_index = 0;
546        self.current_state = None;
547    }
548}
549
550/// Audit log replayer.
551pub struct AuditLogReplayer<S: Clone + Serialize + for<'de> Deserialize<'de>> {
552    /// The audit log entries
553    entries: Vec<StepEntry<S>>,
554    /// Current replay state
555    state: ReplayState<S>,
556}
557
558impl<S: Clone + Serialize + for<'de> Deserialize<'de>> AuditLogReplayer<S> {
559    /// Create a new replayer from audit log.
560    #[must_use]
561    pub fn new(entries: Vec<StepEntry<S>>) -> Self {
562        let total = entries.len();
563        Self {
564            entries,
565            state: ReplayState::new(total),
566        }
567    }
568
569    /// Get current replay state.
570    #[must_use]
571    pub fn state(&self) -> &ReplayState<S> {
572        &self.state
573    }
574
575    /// Get mutable replay state.
576    pub fn state_mut(&mut self) -> &mut ReplayState<S> {
577        &mut self.state
578    }
579
580    /// Get current step entry (if any).
581    #[must_use]
582    pub fn current_entry(&self) -> Option<&StepEntry<S>> {
583        self.entries.get(self.state.current_index)
584    }
585
586    /// Step forward and return the entry.
587    pub fn step_forward(&mut self) -> Option<&StepEntry<S>> {
588        if self.state.paused || self.state.is_complete() {
589            return None;
590        }
591
592        contract_pre_iterator!();
593        let entry = self.entries.get(self.state.current_index);
594        if entry.is_some() {
595            self.state.current_state = entry.map(|e| e.output_state.clone());
596            self.state.advance();
597        }
598        entry
599    }
600
601    /// Seek to a specific step index.
602    pub fn seek_to(&mut self, index: usize) {
603        self.state.seek(index);
604        if let Some(entry) = self.entries.get(index) {
605            self.state.current_state = Some(entry.output_state.clone());
606        }
607    }
608
609    /// Get all entries.
610    #[must_use]
611    pub fn entries(&self) -> &[StepEntry<S>] {
612        &self.entries
613    }
614
615    /// Find entry by step ID.
616    #[must_use]
617    pub fn find_by_step_id(&self, step_id: u64) -> Option<&StepEntry<S>> {
618        contract_pre_iterator!();
619        self.entries.iter().find(|e| e.step_id == step_id)
620    }
621
622    /// Get entries in time range.
623    pub fn entries_in_range(&self, start: SimTime, end: SimTime) -> Vec<&StepEntry<S>> {
624        self.entries
625            .iter()
626            .filter(|e| e.timestamp >= start && e.timestamp <= end)
627            .collect()
628    }
629}
630
631// =============================================================================
632// Verification Helpers
633// =============================================================================
634
635/// Verify RNG state consistency in audit log.
636///
637/// Returns list of step IDs where RNG state is inconsistent.
638pub fn verify_rng_consistency<S: Clone + Serialize + for<'de> Deserialize<'de>>(
639    log: &[StepEntry<S>],
640) -> Vec<u64> {
641    let mut inconsistencies = Vec::new();
642
643    for i in 1..log.len() {
644        // The "after" state of step i-1 should match "before" state of step i
645        if log[i - 1].rng_state_after != log[i].rng_state_before {
646            inconsistencies.push(log[i].step_id);
647        }
648    }
649
650    inconsistencies
651}
652
653/// Compute Blake3 hash of serializable state.
654///
655/// # Errors
656///
657/// Returns error if serialization fails.
658pub fn hash_state<S: Serialize>(state: &S) -> SimResult<[u8; 32]> {
659    let bytes = bincode::serialize(state)
660        .map_err(|e| SimError::serialization(format!("Hash state: {e}")))?;
661    Ok(*blake3::hash(&bytes).as_bytes())
662}
663
664// =============================================================================
665// TSP-Specific Audit Types
666// =============================================================================
667
668/// TSP step type for audit logging.
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
670pub enum TspStepType {
671    /// Initial tour construction
672    Construction,
673    /// 2-opt improvement pass
674    TwoOptPass,
675    /// 2-opt improvement applied
676    TwoOptImprove,
677    /// Best tour updated
678    BestUpdate,
679    /// GRASP iteration complete
680    GraspIteration,
681}
682
683impl std::fmt::Display for TspStepType {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        match self {
686            Self::Construction => write!(f, "construction"),
687            Self::TwoOptPass => write!(f, "two_opt_pass"),
688            Self::TwoOptImprove => write!(f, "two_opt_improve"),
689            Self::BestUpdate => write!(f, "best_update"),
690            Self::GraspIteration => write!(f, "grasp_iteration"),
691        }
692    }
693}
694
695/// TSP state snapshot for audit logging.
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct TspStateSnapshot {
698    /// Current tour (city indices)
699    pub tour: Vec<usize>,
700    /// Current tour length
701    pub tour_length: f64,
702    /// Best tour found
703    pub best_tour: Vec<usize>,
704    /// Best tour length
705    pub best_tour_length: f64,
706    /// Number of restarts
707    pub restarts: u64,
708    /// Number of 2-opt iterations
709    pub two_opt_iterations: u64,
710    /// Number of 2-opt improvements
711    pub two_opt_improvements: u64,
712}
713
714impl Default for TspStateSnapshot {
715    fn default() -> Self {
716        Self {
717            tour: Vec::new(),
718            tour_length: 0.0,
719            best_tour: Vec::new(),
720            best_tour_length: 0.0,
721            restarts: 0,
722            two_opt_iterations: 0,
723            two_opt_improvements: 0,
724        }
725    }
726}
727
728// =============================================================================
729// Tests
730// =============================================================================
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    // =========================================================================
737    // RED PHASE: Failing tests first (EXTREME TDD)
738    // =========================================================================
739
740    // -------------------------------------------------------------------------
741    // EquationEval Tests
742    // -------------------------------------------------------------------------
743
744    #[test]
745    fn test_equation_eval_creation() {
746        let eval = EquationEval::new("two_opt_delta", 0.5);
747
748        assert_eq!(eval.equation_id, "two_opt_delta");
749        assert!((eval.result - 0.5).abs() < f64::EPSILON);
750        assert!(eval.inputs.is_empty());
751        assert!(eval.expected.is_none());
752        assert!(eval.error.is_none());
753    }
754
755    #[test]
756    fn test_equation_eval_with_inputs() {
757        let eval = EquationEval::new("two_opt_delta", 0.5)
758            .with_input("d_i_i1", 1.0)
759            .with_input("d_j_j1", 0.8)
760            .with_input("d_i_j", 0.7)
761            .with_input("d_i1_j1", 0.6);
762
763        assert_eq!(eval.inputs.len(), 4);
764        assert!((eval.inputs["d_i_i1"] - 1.0).abs() < f64::EPSILON);
765        assert!((eval.inputs["d_j_j1"] - 0.8).abs() < f64::EPSILON);
766    }
767
768    #[test]
769    fn test_equation_eval_with_expected() {
770        let eval = EquationEval::new("tour_length", 4.5).with_expected(4.48);
771
772        assert!(eval.expected.is_some());
773        assert!((eval.expected.unwrap() - 4.48).abs() < f64::EPSILON);
774        assert!(eval.error.is_some());
775        // Use tolerance since 4.5 - 4.48 = 0.020000000000000018 due to floating point
776        assert!((eval.error.unwrap() - 0.02).abs() < 1e-10);
777    }
778
779    #[test]
780    fn test_equation_eval_is_correct() {
781        let eval_correct = EquationEval::new("test", 1.0).with_expected(1.001);
782        let eval_incorrect = EquationEval::new("test", 1.0).with_expected(2.0);
783
784        assert!(eval_correct.is_correct(0.01));
785        assert!(!eval_incorrect.is_correct(0.01));
786    }
787
788    #[test]
789    fn test_equation_eval_z3_verified() {
790        let eval = EquationEval::new("test", 1.0).with_z3_verified(true);
791
792        assert_eq!(eval.z3_verified, Some(true));
793    }
794
795    // -------------------------------------------------------------------------
796    // Decision Tests
797    // -------------------------------------------------------------------------
798
799    #[test]
800    fn test_decision_creation() {
801        let decision = Decision::new("rcl_selection", "city_7");
802
803        assert_eq!(decision.decision_type, "rcl_selection");
804        assert_eq!(decision.chosen, "city_7");
805        assert!(decision.options.is_empty());
806        assert!(decision.rationale.is_empty());
807    }
808
809    #[test]
810    fn test_decision_with_options() {
811        let decision = Decision::new("rcl_selection", "city_7").with_options(vec![
812            "city_3".into(),
813            "city_7".into(),
814            "city_12".into(),
815        ]);
816
817        assert_eq!(decision.options.len(), 3);
818        assert!(decision.options.contains(&"city_7".to_string()));
819    }
820
821    #[test]
822    fn test_decision_with_rationale() {
823        let decision = Decision::new("two_opt_apply", "apply")
824            .with_rationale("delta", 0.15)
825            .with_rationale("new_length", 4.35);
826
827        assert_eq!(decision.rationale.len(), 2);
828        assert!((decision.rationale["delta"] - 0.15).abs() < f64::EPSILON);
829    }
830
831    #[test]
832    fn test_decision_with_rng_value() {
833        let decision = Decision::new("rcl_selection", "city_7").with_rng_value(0.42);
834
835        assert_eq!(decision.rng_value, Some(0.42));
836    }
837
838    // -------------------------------------------------------------------------
839    // StepEntry Tests
840    // -------------------------------------------------------------------------
841
842    #[test]
843    fn test_step_entry_creation() {
844        let entry: StepEntry<u64> =
845            StepEntry::new(1, SimTime::from_secs(0.1), "construction", 0u64, 1u64);
846
847        assert_eq!(entry.step_id, 1);
848        assert_eq!(entry.step_type, "construction");
849        assert_eq!(entry.input_state, 0);
850        assert_eq!(entry.output_state, 1);
851    }
852
853    #[test]
854    fn test_step_entry_with_rng_states() {
855        let before = [1u8; 32];
856        let after = [2u8; 32];
857
858        let entry: StepEntry<u64> = StepEntry::new(1, SimTime::from_secs(0.1), "test", 0u64, 1u64)
859            .with_rng_states(before, after);
860
861        assert_eq!(entry.rng_state_before, before);
862        assert_eq!(entry.rng_state_after, after);
863    }
864
865    #[test]
866    fn test_step_entry_add_equation_eval() {
867        let mut entry: StepEntry<u64> =
868            StepEntry::new(1, SimTime::from_secs(0.1), "test", 0u64, 1u64);
869
870        entry.add_equation_eval(EquationEval::new("test_eq", 1.5));
871
872        assert_eq!(entry.equation_evaluations.len(), 1);
873        assert_eq!(entry.equation_evaluations[0].equation_id, "test_eq");
874    }
875
876    #[test]
877    fn test_step_entry_add_decision() {
878        let mut entry: StepEntry<u64> =
879            StepEntry::new(1, SimTime::from_secs(0.1), "test", 0u64, 1u64);
880
881        entry.add_decision(Decision::new("test_decision", "option_a"));
882
883        assert_eq!(entry.decisions.len(), 1);
884        assert_eq!(entry.decisions[0].decision_type, "test_decision");
885    }
886
887    #[test]
888    fn test_step_entry_with_duration() {
889        let entry: StepEntry<u64> =
890            StepEntry::new(1, SimTime::from_secs(0.1), "test", 0u64, 1u64).with_duration(1000);
891
892        assert_eq!(entry.compute_duration_us, 1000);
893    }
894
895    // -------------------------------------------------------------------------
896    // GeneratedTestCase Tests
897    // -------------------------------------------------------------------------
898
899    #[test]
900    fn test_generated_test_case_creation() {
901        let tc = GeneratedTestCase::new("test_two_opt_1", "two_opt_delta", 0.5, 42);
902
903        assert_eq!(tc.name, "test_two_opt_1");
904        assert_eq!(tc.equation_id, "two_opt_delta");
905        assert!((tc.expected_output - 0.5).abs() < f64::EPSILON);
906        assert_eq!(tc.source_step_id, 42);
907    }
908
909    #[test]
910    fn test_generated_test_case_with_inputs() {
911        let tc = GeneratedTestCase::new("test", "eq", 1.0, 1)
912            .with_input("x", 2.0)
913            .with_input("y", 3.0);
914
915        assert_eq!(tc.inputs.len(), 2);
916        assert!((tc.inputs["x"] - 2.0).abs() < f64::EPSILON);
917    }
918
919    #[test]
920    fn test_generated_test_case_to_rust() {
921        let tc = GeneratedTestCase::new("test_eq_1", "my_equation", 42.0, 5)
922            .with_input("a", 1.0)
923            .with_assertion("equation should compute correctly");
924
925        let rust_code = tc.to_rust_test();
926
927        assert!(rust_code.contains("#[test]"));
928        assert!(rust_code.contains("fn test_eq_1()"));
929        assert!(rust_code.contains("let a = 1"));
930        assert!(rust_code.contains("let expected = 42"));
931    }
932
933    // -------------------------------------------------------------------------
934    // ReplayState Tests
935    // -------------------------------------------------------------------------
936
937    #[test]
938    fn test_replay_state_creation() {
939        let state: ReplayState<u64> = ReplayState::new(100);
940
941        assert_eq!(state.current_index, 0);
942        assert_eq!(state.total_steps, 100);
943        assert!(!state.is_complete());
944        assert!(!state.paused);
945    }
946
947    #[test]
948    fn test_replay_state_advance() {
949        let mut state: ReplayState<u64> = ReplayState::new(10);
950
951        state.advance();
952        assert_eq!(state.current_index, 1);
953
954        state.advance();
955        assert_eq!(state.current_index, 2);
956    }
957
958    #[test]
959    fn test_replay_state_progress() {
960        let mut state: ReplayState<u64> = ReplayState::new(10);
961
962        assert!((state.progress_percent() - 0.0).abs() < f64::EPSILON);
963
964        state.seek(5);
965        assert!((state.progress_percent() - 50.0).abs() < f64::EPSILON);
966
967        state.seek(10);
968        assert!((state.progress_percent() - 100.0).abs() < f64::EPSILON);
969    }
970
971    #[test]
972    fn test_replay_state_is_complete() {
973        let mut state: ReplayState<u64> = ReplayState::new(3);
974
975        assert!(!state.is_complete());
976
977        state.seek(3);
978        assert!(state.is_complete());
979    }
980
981    #[test]
982    fn test_replay_state_reset() {
983        let mut state: ReplayState<u64> = ReplayState::new(10);
984        state.seek(5);
985        state.current_state = Some(42);
986
987        state.reset();
988
989        assert_eq!(state.current_index, 0);
990        assert!(state.current_state.is_none());
991    }
992
993    // -------------------------------------------------------------------------
994    // AuditLogReplayer Tests
995    // -------------------------------------------------------------------------
996
997    #[test]
998    fn test_replayer_creation() {
999        let entries: Vec<StepEntry<u64>> = vec![
1000            StepEntry::new(0, SimTime::ZERO, "step_0", 0, 1),
1001            StepEntry::new(1, SimTime::from_secs(0.1), "step_1", 1, 2),
1002        ];
1003
1004        let replayer = AuditLogReplayer::new(entries);
1005
1006        assert_eq!(replayer.entries().len(), 2);
1007        assert_eq!(replayer.state().total_steps, 2);
1008    }
1009
1010    #[test]
1011    fn test_replayer_step_forward() {
1012        let entries: Vec<StepEntry<u64>> = vec![
1013            StepEntry::new(0, SimTime::ZERO, "step_0", 0, 1),
1014            StepEntry::new(1, SimTime::from_secs(0.1), "step_1", 1, 2),
1015        ];
1016
1017        let mut replayer = AuditLogReplayer::new(entries);
1018
1019        let entry = replayer.step_forward();
1020        assert!(entry.is_some());
1021        assert_eq!(entry.unwrap().step_id, 0);
1022
1023        let entry = replayer.step_forward();
1024        assert!(entry.is_some());
1025        assert_eq!(entry.unwrap().step_id, 1);
1026
1027        // Should return None when complete
1028        let entry = replayer.step_forward();
1029        assert!(entry.is_none());
1030    }
1031
1032    #[test]
1033    fn test_replayer_seek() {
1034        let entries: Vec<StepEntry<u64>> = vec![
1035            StepEntry::new(0, SimTime::ZERO, "step_0", 0, 1),
1036            StepEntry::new(1, SimTime::from_secs(0.1), "step_1", 1, 2),
1037            StepEntry::new(2, SimTime::from_secs(0.2), "step_2", 2, 3),
1038        ];
1039
1040        let mut replayer = AuditLogReplayer::new(entries);
1041
1042        replayer.seek_to(2);
1043        assert_eq!(replayer.state().current_index, 2);
1044        assert_eq!(replayer.state().current_state, Some(3));
1045    }
1046
1047    #[test]
1048    fn test_replayer_find_by_step_id() {
1049        let entries: Vec<StepEntry<u64>> = vec![
1050            StepEntry::new(10, SimTime::ZERO, "step_0", 0, 1),
1051            StepEntry::new(20, SimTime::from_secs(0.1), "step_1", 1, 2),
1052        ];
1053
1054        let replayer = AuditLogReplayer::new(entries);
1055
1056        let found = replayer.find_by_step_id(20);
1057        assert!(found.is_some());
1058        assert_eq!(found.unwrap().step_id, 20);
1059
1060        let not_found = replayer.find_by_step_id(99);
1061        assert!(not_found.is_none());
1062    }
1063
1064    // -------------------------------------------------------------------------
1065    // Verification Helper Tests
1066    // -------------------------------------------------------------------------
1067
1068    #[test]
1069    fn test_verify_rng_consistency_valid() {
1070        let entries: Vec<StepEntry<u64>> = vec![
1071            StepEntry::new(0, SimTime::ZERO, "s0", 0, 1).with_rng_states([1; 32], [2; 32]),
1072            StepEntry::new(1, SimTime::from_secs(0.1), "s1", 1, 2)
1073                .with_rng_states([2; 32], [3; 32]),
1074            StepEntry::new(2, SimTime::from_secs(0.2), "s2", 2, 3)
1075                .with_rng_states([3; 32], [4; 32]),
1076        ];
1077
1078        let inconsistencies = verify_rng_consistency(&entries);
1079        assert!(inconsistencies.is_empty());
1080    }
1081
1082    #[test]
1083    fn test_verify_rng_consistency_invalid() {
1084        let entries: Vec<StepEntry<u64>> = vec![
1085            StepEntry::new(0, SimTime::ZERO, "s0", 0, 1).with_rng_states([1; 32], [2; 32]),
1086            StepEntry::new(1, SimTime::from_secs(0.1), "s1", 1, 2)
1087                .with_rng_states([99; 32], [3; 32]), // Inconsistent!
1088        ];
1089
1090        let inconsistencies = verify_rng_consistency(&entries);
1091        assert_eq!(inconsistencies.len(), 1);
1092        assert_eq!(inconsistencies[0], 1);
1093    }
1094
1095    #[test]
1096    fn test_hash_state() {
1097        let state1 = 42u64;
1098        let state2 = 42u64;
1099        let state3 = 43u64;
1100
1101        let hash1 = hash_state(&state1).expect("hash");
1102        let hash2 = hash_state(&state2).expect("hash");
1103        let hash3 = hash_state(&state3).expect("hash");
1104
1105        assert_eq!(hash1, hash2); // Same state = same hash
1106        assert_ne!(hash1, hash3); // Different state = different hash
1107    }
1108
1109    // -------------------------------------------------------------------------
1110    // SimulationAuditLog Trait Tests (via mock implementation)
1111    // -------------------------------------------------------------------------
1112
1113    struct MockSimulation {
1114        log: Vec<StepEntry<u64>>,
1115    }
1116
1117    impl MockSimulation {
1118        fn new() -> Self {
1119            Self { log: Vec::new() }
1120        }
1121    }
1122
1123    impl SimulationAuditLog for MockSimulation {
1124        type StateSnapshot = u64;
1125
1126        fn log_step(&mut self, entry: StepEntry<Self::StateSnapshot>) {
1127            self.log.push(entry);
1128        }
1129
1130        fn audit_log(&self) -> &[StepEntry<Self::StateSnapshot>] {
1131            &self.log
1132        }
1133
1134        fn audit_log_mut(&mut self) -> &mut Vec<StepEntry<Self::StateSnapshot>> {
1135            &mut self.log
1136        }
1137
1138        fn clear_audit_log(&mut self) {
1139            self.log.clear();
1140        }
1141    }
1142
1143    #[test]
1144    fn test_simulation_audit_log_trait() {
1145        let mut sim = MockSimulation::new();
1146
1147        let mut entry = StepEntry::new(0, SimTime::ZERO, "test", 0u64, 1u64);
1148        entry.add_equation_eval(EquationEval::new("eq1", 1.0).with_expected(1.0));
1149        entry.add_decision(Decision::new("decide", "a"));
1150
1151        sim.log_step(entry);
1152
1153        assert_eq!(sim.audit_log().len(), 1);
1154        assert_eq!(sim.total_equation_evals(), 1);
1155        assert_eq!(sim.total_decisions(), 1);
1156    }
1157
1158    #[test]
1159    fn test_simulation_audit_log_export_json() {
1160        let mut sim = MockSimulation::new();
1161        sim.log_step(StepEntry::new(0, SimTime::ZERO, "test", 0u64, 1u64));
1162
1163        let json = sim.export_audit_json().expect("json export");
1164        assert!(json.contains("step_id"));
1165        assert!(json.contains("\"step_id\": 0"));
1166    }
1167
1168    #[test]
1169    fn test_simulation_audit_log_generate_tests() {
1170        let mut sim = MockSimulation::new();
1171
1172        let mut entry = StepEntry::new(0, SimTime::ZERO, "test", 0u64, 1u64);
1173        entry.add_equation_eval(
1174            EquationEval::new("my_equation", 42.0)
1175                .with_input("x", 1.0)
1176                .with_input("y", 2.0),
1177        );
1178        sim.log_step(entry);
1179
1180        let test_cases = sim.generate_test_cases();
1181
1182        assert_eq!(test_cases.len(), 1);
1183        assert_eq!(test_cases[0].equation_id, "my_equation");
1184        assert!((test_cases[0].expected_output - 42.0).abs() < f64::EPSILON);
1185    }
1186
1187    #[test]
1188    fn test_simulation_audit_log_verify_equations() {
1189        let mut sim = MockSimulation::new();
1190
1191        let mut entry1 = StepEntry::new(0, SimTime::ZERO, "test", 0u64, 1u64);
1192        entry1.add_equation_eval(EquationEval::new("eq1", 1.0).with_expected(1.0)); // Correct
1193
1194        let mut entry2 = StepEntry::new(1, SimTime::from_secs(0.1), "test", 1u64, 2u64);
1195        entry2.add_equation_eval(EquationEval::new("eq2", 1.0).with_expected(2.0)); // Wrong!
1196
1197        sim.log_step(entry1);
1198        sim.log_step(entry2);
1199
1200        let failures = sim.verify_all_equations(0.001);
1201
1202        assert_eq!(failures.len(), 1);
1203        assert_eq!(failures[0].0, 1); // Step 1 failed
1204        assert_eq!(failures[0].1, "eq2"); // Equation eq2
1205    }
1206
1207    #[test]
1208    fn test_simulation_audit_log_clear() {
1209        let mut sim = MockSimulation::new();
1210        sim.log_step(StepEntry::new(0, SimTime::ZERO, "test", 0u64, 1u64));
1211
1212        assert_eq!(sim.audit_log().len(), 1);
1213
1214        sim.clear_audit_log();
1215        assert!(sim.audit_log().is_empty());
1216    }
1217
1218    // -------------------------------------------------------------------------
1219    // Serialization Tests
1220    // -------------------------------------------------------------------------
1221
1222    #[test]
1223    fn test_equation_eval_serialization() {
1224        let eval = EquationEval::new("test", 1.5)
1225            .with_input("x", 1.0)
1226            .with_expected(1.5);
1227
1228        let json = serde_json::to_string(&eval).expect("serialize");
1229        let restored: EquationEval = serde_json::from_str(&json).expect("deserialize");
1230
1231        assert_eq!(restored.equation_id, eval.equation_id);
1232        assert!((restored.result - eval.result).abs() < f64::EPSILON);
1233    }
1234
1235    #[test]
1236    fn test_decision_serialization() {
1237        let decision = Decision::new("test", "a")
1238            .with_options(vec!["a".into(), "b".into()])
1239            .with_rationale("score", 0.5);
1240
1241        let json = serde_json::to_string(&decision).expect("serialize");
1242        let restored: Decision = serde_json::from_str(&json).expect("deserialize");
1243
1244        assert_eq!(restored.decision_type, decision.decision_type);
1245        assert_eq!(restored.chosen, decision.chosen);
1246    }
1247
1248    #[test]
1249    fn test_step_entry_serialization() {
1250        let entry: StepEntry<u64> =
1251            StepEntry::new(1, SimTime::from_secs(0.5), "test", 10u64, 20u64);
1252
1253        let json = serde_json::to_string(&entry).expect("serialize");
1254        let restored: StepEntry<u64> = serde_json::from_str(&json).expect("deserialize");
1255
1256        assert_eq!(restored.step_id, entry.step_id);
1257        assert_eq!(restored.input_state, entry.input_state);
1258        assert_eq!(restored.output_state, entry.output_state);
1259    }
1260}