Skip to main content

jugar_probar/
replay.rs

1//! Enhanced Deterministic Replay System (Feature 23 - EDD Compliance)
2//!
3//! Provides advanced deterministic replay capabilities for WASM games.
4//! Supports recording, replaying, and verifying game sessions with
5//! frame-accurate determinism.
6//!
7//! ## EXTREME TDD: Tests written FIRST per spec
8//!
9//! ## Toyota Way Application
10//!
11//! - **Poka-Yoke**: Type-safe replay files with version checking
12//! - **Muda**: Efficient binary/YAML serialization
13//! - **Genchi Genbutsu**: Frame-by-frame state verification
14//! - **Jidoka**: Fail-fast on determinism violations
15
16use crate::event::InputEvent;
17use crate::result::{ProbarError, ProbarResult};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::collections::HashMap;
21use std::fs;
22use std::path::Path;
23
24/// Version of the replay format
25pub const REPLAY_FORMAT_VERSION: u32 = 1;
26
27/// Replay file header
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ReplayHeader {
30    /// Format version
31    pub version: u32,
32    /// Name of the game/application
33    pub game_name: String,
34    /// Game version
35    pub game_version: String,
36    /// Replay creation timestamp (Unix epoch)
37    pub created_at: u64,
38    /// Initial random seed
39    pub seed: u64,
40    /// Total number of frames
41    pub total_frames: u64,
42    /// Target FPS
43    pub fps: u32,
44    /// Checksum of replay data
45    pub checksum: String,
46}
47
48impl ReplayHeader {
49    /// Create a new replay header
50    #[must_use]
51    pub fn new(game_name: &str, game_version: &str, seed: u64) -> Self {
52        Self {
53            version: REPLAY_FORMAT_VERSION,
54            game_name: game_name.to_string(),
55            game_version: game_version.to_string(),
56            created_at: std::time::SystemTime::now()
57                .duration_since(std::time::UNIX_EPOCH)
58                .unwrap_or_default()
59                .as_secs(),
60            seed,
61            total_frames: 0,
62            fps: 60,
63            checksum: String::new(),
64        }
65    }
66
67    /// Set the FPS
68    #[must_use]
69    pub const fn with_fps(mut self, fps: u32) -> Self {
70        self.fps = fps;
71        self
72    }
73}
74
75/// A single input event with frame timing
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TimedInput {
78    /// Frame number when input occurred
79    pub frame: u64,
80    /// The input event
81    pub event: InputEvent,
82}
83
84impl TimedInput {
85    /// Create a new timed input
86    #[must_use]
87    pub const fn new(frame: u64, event: InputEvent) -> Self {
88        Self { frame, event }
89    }
90}
91
92/// State checkpoint for verification
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct StateCheckpoint {
95    /// Frame number
96    pub frame: u64,
97    /// Hash of the game state at this frame
98    pub state_hash: String,
99    /// Optional state data (for debugging)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub state_data: Option<HashMap<String, serde_json::Value>>,
102}
103
104impl StateCheckpoint {
105    /// Create a new checkpoint with hash
106    #[must_use]
107    pub fn new(frame: u64, state_hash: &str) -> Self {
108        Self {
109            frame,
110            state_hash: state_hash.to_string(),
111            state_data: None,
112        }
113    }
114
115    /// Create a checkpoint with state data
116    #[must_use]
117    pub fn with_data(
118        frame: u64,
119        state_hash: &str,
120        data: HashMap<String, serde_json::Value>,
121    ) -> Self {
122        Self {
123            frame,
124            state_hash: state_hash.to_string(),
125            state_data: Some(data),
126        }
127    }
128}
129
130/// A complete replay recording
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Replay {
133    /// Replay header
134    pub header: ReplayHeader,
135    /// All timed inputs
136    pub inputs: Vec<TimedInput>,
137    /// State checkpoints (for verification)
138    pub checkpoints: Vec<StateCheckpoint>,
139    /// Metadata
140    #[serde(default)]
141    pub metadata: HashMap<String, String>,
142}
143
144impl Replay {
145    /// Create a new replay
146    #[must_use]
147    pub fn new(header: ReplayHeader) -> Self {
148        Self {
149            header,
150            inputs: Vec::new(),
151            checkpoints: Vec::new(),
152            metadata: HashMap::new(),
153        }
154    }
155
156    /// Add an input event
157    pub fn add_input(&mut self, frame: u64, event: InputEvent) {
158        self.inputs.push(TimedInput::new(frame, event));
159        self.header.total_frames = self.header.total_frames.max(frame + 1);
160    }
161
162    /// Add a state checkpoint
163    pub fn add_checkpoint(&mut self, checkpoint: StateCheckpoint) {
164        self.header.total_frames = self.header.total_frames.max(checkpoint.frame + 1);
165        self.checkpoints.push(checkpoint);
166    }
167
168    /// Add metadata
169    pub fn set_metadata(&mut self, key: &str, value: &str) {
170        self.metadata.insert(key.to_string(), value.to_string());
171    }
172
173    /// Get inputs for a specific frame
174    #[must_use]
175    pub fn inputs_at_frame(&self, frame: u64) -> Vec<&InputEvent> {
176        self.inputs
177            .iter()
178            .filter(|i| i.frame == frame)
179            .map(|i| &i.event)
180            .collect()
181    }
182
183    /// Get checkpoint at or before a frame
184    #[must_use]
185    pub fn checkpoint_at_or_before(&self, frame: u64) -> Option<&StateCheckpoint> {
186        self.checkpoints
187            .iter()
188            .filter(|c| c.frame <= frame)
189            .max_by_key(|c| c.frame)
190    }
191
192    /// Compute checksum of replay data
193    #[must_use]
194    pub fn compute_checksum(&self) -> String {
195        let mut hasher = Sha256::new();
196
197        // Hash header fields
198        hasher.update(self.header.seed.to_le_bytes());
199        hasher.update(self.header.total_frames.to_le_bytes());
200        hasher.update(self.header.fps.to_le_bytes());
201
202        // Hash inputs
203        for input in &self.inputs {
204            hasher.update(input.frame.to_le_bytes());
205            hasher.update(format!("{:?}", input.event).as_bytes());
206        }
207
208        // Hash checkpoints
209        for checkpoint in &self.checkpoints {
210            hasher.update(checkpoint.frame.to_le_bytes());
211            hasher.update(checkpoint.state_hash.as_bytes());
212        }
213
214        let result = hasher.finalize();
215        format!("{result:x}")
216    }
217
218    /// Finalize the replay (compute checksum)
219    pub fn finalize(&mut self) {
220        self.header.checksum = self.compute_checksum();
221    }
222
223    /// Verify replay checksum
224    #[must_use]
225    pub fn verify_checksum(&self) -> bool {
226        // Create a copy without checksum to compute
227        let computed = self.compute_checksum();
228        computed == self.header.checksum
229    }
230
231    /// Save replay to YAML file
232    pub fn save_yaml(&self, path: &Path) -> ProbarResult<()> {
233        let yaml = serde_yaml_ng::to_string(self).map_err(|e| {
234            ProbarError::SnapshotSerializationError {
235                message: format!("Failed to serialize replay: {e}"),
236            }
237        })?;
238
239        if let Some(parent) = path.parent() {
240            fs::create_dir_all(parent)?;
241        }
242
243        fs::write(path, yaml)?;
244        Ok(())
245    }
246
247    /// Load replay from YAML file
248    pub fn load_yaml(path: &Path) -> ProbarResult<Self> {
249        let yaml = fs::read_to_string(path)?;
250        let replay: Replay = serde_yaml_ng::from_str(&yaml).map_err(|e| {
251            ProbarError::SnapshotSerializationError {
252                message: format!("Failed to deserialize replay: {e}"),
253            }
254        })?;
255        Ok(replay)
256    }
257
258    /// Save replay to JSON file
259    pub fn save_json(&self, path: &Path) -> ProbarResult<()> {
260        let json = serde_json::to_string_pretty(self)?;
261
262        if let Some(parent) = path.parent() {
263            fs::create_dir_all(parent)?;
264        }
265
266        fs::write(path, json)?;
267        Ok(())
268    }
269
270    /// Load replay from JSON file
271    pub fn load_json(path: &Path) -> ProbarResult<Self> {
272        let json = fs::read_to_string(path)?;
273        let replay: Replay = serde_json::from_str(&json)?;
274        Ok(replay)
275    }
276}
277
278/// Replay recorder for capturing gameplay
279#[derive(Debug)]
280pub struct ReplayRecorder {
281    /// The replay being recorded
282    replay: Replay,
283    /// Current frame
284    current_frame: u64,
285    /// Checkpoint interval (frames between checkpoints)
286    checkpoint_interval: u64,
287    /// Whether recording is active
288    recording: bool,
289}
290
291impl ReplayRecorder {
292    /// Create a new replay recorder
293    #[must_use]
294    pub fn new(game_name: &str, game_version: &str, seed: u64) -> Self {
295        let header = ReplayHeader::new(game_name, game_version, seed);
296        Self {
297            replay: Replay::new(header),
298            current_frame: 0,
299            checkpoint_interval: 60, // Default: checkpoint every second at 60fps
300            recording: true,
301        }
302    }
303
304    /// Set the checkpoint interval
305    #[must_use]
306    pub const fn with_checkpoint_interval(mut self, interval: u64) -> Self {
307        self.checkpoint_interval = interval;
308        self
309    }
310
311    /// Set FPS
312    #[must_use]
313    pub fn with_fps(mut self, fps: u32) -> Self {
314        self.replay.header = self.replay.header.with_fps(fps);
315        self
316    }
317
318    /// Record an input event
319    pub fn record_input(&mut self, event: InputEvent) {
320        if self.recording {
321            self.replay.add_input(self.current_frame, event);
322        }
323    }
324
325    /// Record multiple input events
326    pub fn record_inputs(&mut self, events: &[InputEvent]) {
327        for event in events {
328            self.record_input(event.clone());
329        }
330    }
331
332    /// Advance to next frame and optionally record checkpoint
333    pub fn next_frame(&mut self, state_hash: Option<&str>) {
334        self.current_frame += 1;
335
336        // Record checkpoint if at interval
337        if let Some(hash) = state_hash {
338            if self.current_frame % self.checkpoint_interval == 0 {
339                self.replay
340                    .add_checkpoint(StateCheckpoint::new(self.current_frame, hash));
341            }
342        }
343    }
344
345    /// Record a checkpoint at current frame
346    pub fn checkpoint(&mut self, state_hash: &str) {
347        self.replay
348            .add_checkpoint(StateCheckpoint::new(self.current_frame, state_hash));
349    }
350
351    /// Record a checkpoint with state data
352    pub fn checkpoint_with_data(
353        &mut self,
354        state_hash: &str,
355        data: HashMap<String, serde_json::Value>,
356    ) {
357        self.replay.add_checkpoint(StateCheckpoint::with_data(
358            self.current_frame,
359            state_hash,
360            data,
361        ));
362    }
363
364    /// Stop recording
365    pub fn stop(&mut self) {
366        self.recording = false;
367    }
368
369    /// Finalize and get the replay
370    #[must_use]
371    pub fn finalize(mut self) -> Replay {
372        self.replay.finalize();
373        self.replay
374    }
375
376    /// Get current frame
377    #[must_use]
378    pub const fn current_frame(&self) -> u64 {
379        self.current_frame
380    }
381
382    /// Check if recording
383    #[must_use]
384    pub const fn is_recording(&self) -> bool {
385        self.recording
386    }
387}
388
389/// Replay playback controller
390#[derive(Debug)]
391pub struct ReplayPlayer {
392    /// The replay being played
393    replay: Replay,
394    /// Current frame
395    current_frame: u64,
396    /// Playback speed (1.0 = normal)
397    speed: f64,
398    /// Whether playback is active
399    playing: bool,
400    /// Index into inputs array for efficiency
401    input_index: usize,
402}
403
404impl ReplayPlayer {
405    /// Create a new replay player
406    #[must_use]
407    pub fn new(replay: Replay) -> Self {
408        Self {
409            replay,
410            current_frame: 0,
411            speed: 1.0,
412            playing: true,
413            input_index: 0,
414        }
415    }
416
417    /// Set playback speed
418    #[must_use]
419    pub fn with_speed(mut self, speed: f64) -> Self {
420        self.speed = speed;
421        self
422    }
423
424    /// Get inputs for current frame and advance
425    #[must_use]
426    pub fn get_frame_inputs(&mut self) -> Vec<InputEvent> {
427        if !self.playing {
428            return Vec::new();
429        }
430
431        let mut inputs = Vec::new();
432
433        // Collect all inputs for current frame
434        while self.input_index < self.replay.inputs.len() {
435            let timed = &self.replay.inputs[self.input_index];
436            if timed.frame == self.current_frame {
437                inputs.push(timed.event.clone());
438                self.input_index += 1;
439            } else if timed.frame > self.current_frame {
440                break;
441            } else {
442                self.input_index += 1;
443            }
444        }
445
446        self.current_frame += 1;
447
448        // Check if replay is done
449        if self.current_frame >= self.replay.header.total_frames {
450            self.playing = false;
451        }
452
453        inputs
454    }
455
456    /// Get expected state hash for current frame (if checkpoint exists)
457    #[must_use]
458    pub fn expected_checkpoint(&self) -> Option<&StateCheckpoint> {
459        self.replay
460            .checkpoints
461            .iter()
462            .find(|c| c.frame == self.current_frame - 1)
463    }
464
465    /// Verify state against checkpoint
466    pub fn verify_state(&self, state_hash: &str) -> ProbarResult<()> {
467        if let Some(checkpoint) = self.expected_checkpoint() {
468            if checkpoint.state_hash != state_hash {
469                return Err(ProbarError::AssertionFailed {
470                    message: format!(
471                        "State divergence at frame {}: expected hash '{}', got '{}'",
472                        checkpoint.frame, checkpoint.state_hash, state_hash
473                    ),
474                });
475            }
476        }
477        Ok(())
478    }
479
480    /// Get current frame
481    #[must_use]
482    pub const fn current_frame(&self) -> u64 {
483        self.current_frame
484    }
485
486    /// Check if playback is active
487    #[must_use]
488    pub const fn is_playing(&self) -> bool {
489        self.playing
490    }
491
492    /// Get total frames in replay
493    #[must_use]
494    pub const fn total_frames(&self) -> u64 {
495        self.replay.header.total_frames
496    }
497
498    /// Get progress (0.0 to 1.0)
499    #[must_use]
500    pub fn progress(&self) -> f64 {
501        if self.replay.header.total_frames == 0 {
502            return 1.0;
503        }
504        self.current_frame as f64 / self.replay.header.total_frames as f64
505    }
506
507    /// Seek to a specific frame
508    pub fn seek(&mut self, frame: u64) {
509        self.current_frame = frame.min(self.replay.header.total_frames);
510        self.playing = self.current_frame < self.replay.header.total_frames;
511
512        // Reset input index and search for correct position
513        self.input_index = 0;
514        while self.input_index < self.replay.inputs.len()
515            && self.replay.inputs[self.input_index].frame < self.current_frame
516        {
517            self.input_index += 1;
518        }
519    }
520
521    /// Pause playback
522    pub fn pause(&mut self) {
523        self.playing = false;
524    }
525
526    /// Resume playback
527    pub fn resume(&mut self) {
528        if self.current_frame < self.replay.header.total_frames {
529            self.playing = true;
530        }
531    }
532
533    /// Get the underlying replay
534    #[must_use]
535    pub fn replay(&self) -> &Replay {
536        &self.replay
537    }
538}
539
540/// Result of replay verification
541#[derive(Debug, Clone)]
542pub struct VerificationResult {
543    /// Whether verification passed
544    pub passed: bool,
545    /// Number of frames verified
546    pub frames_verified: u64,
547    /// Number of checkpoints verified
548    pub checkpoints_verified: usize,
549    /// Frame where divergence occurred (if any)
550    pub divergence_frame: Option<u64>,
551    /// Divergence details
552    pub divergence_details: Option<String>,
553}
554
555impl VerificationResult {
556    /// Create a successful verification result
557    #[must_use]
558    pub const fn success(frames_verified: u64, checkpoints_verified: usize) -> Self {
559        Self {
560            passed: true,
561            frames_verified,
562            checkpoints_verified,
563            divergence_frame: None,
564            divergence_details: None,
565        }
566    }
567
568    /// Create a failed verification result
569    #[must_use]
570    pub fn failure(frame: u64, details: &str) -> Self {
571        Self {
572            passed: false,
573            frames_verified: frame,
574            checkpoints_verified: 0,
575            divergence_frame: Some(frame),
576            divergence_details: Some(details.to_string()),
577        }
578    }
579}
580
581#[cfg(test)]
582#[allow(clippy::unwrap_used, clippy::expect_used)]
583mod tests {
584    use super::*;
585
586    mod replay_header_tests {
587        use super::*;
588
589        #[test]
590        fn test_new() {
591            let header = ReplayHeader::new("test_game", "1.0.0", 42);
592            assert_eq!(header.game_name, "test_game");
593            assert_eq!(header.game_version, "1.0.0");
594            assert_eq!(header.seed, 42);
595            assert_eq!(header.version, REPLAY_FORMAT_VERSION);
596        }
597
598        #[test]
599        fn test_with_fps() {
600            let header = ReplayHeader::new("game", "1.0", 0).with_fps(30);
601            assert_eq!(header.fps, 30);
602        }
603    }
604
605    mod timed_input_tests {
606        use super::*;
607
608        #[test]
609        fn test_new() {
610            let event = InputEvent::key_press("Space");
611            let timed = TimedInput::new(100, event);
612            assert_eq!(timed.frame, 100);
613        }
614    }
615
616    mod state_checkpoint_tests {
617        use super::*;
618
619        #[test]
620        fn test_new() {
621            let cp = StateCheckpoint::new(50, "abc123");
622            assert_eq!(cp.frame, 50);
623            assert_eq!(cp.state_hash, "abc123");
624            assert!(cp.state_data.is_none());
625        }
626
627        #[test]
628        fn test_with_data() {
629            let mut data = HashMap::new();
630            data.insert("score".to_string(), serde_json::json!(100));
631            let cp = StateCheckpoint::with_data(50, "abc123", data);
632            assert!(cp.state_data.is_some());
633        }
634    }
635
636    mod replay_tests {
637        use super::*;
638
639        #[test]
640        fn test_new() {
641            let header = ReplayHeader::new("game", "1.0", 42);
642            let replay = Replay::new(header);
643            assert!(replay.inputs.is_empty());
644            assert!(replay.checkpoints.is_empty());
645        }
646
647        #[test]
648        fn test_add_input() {
649            let header = ReplayHeader::new("game", "1.0", 42);
650            let mut replay = Replay::new(header);
651
652            replay.add_input(0, InputEvent::key_press("A"));
653            replay.add_input(10, InputEvent::key_press("B"));
654
655            assert_eq!(replay.inputs.len(), 2);
656            assert_eq!(replay.header.total_frames, 11);
657        }
658
659        #[test]
660        fn test_inputs_at_frame() {
661            let header = ReplayHeader::new("game", "1.0", 42);
662            let mut replay = Replay::new(header);
663
664            replay.add_input(5, InputEvent::key_press("A"));
665            replay.add_input(5, InputEvent::key_press("B"));
666            replay.add_input(10, InputEvent::key_press("C"));
667
668            let inputs = replay.inputs_at_frame(5);
669            assert_eq!(inputs.len(), 2);
670        }
671
672        #[test]
673        fn test_add_checkpoint() {
674            let header = ReplayHeader::new("game", "1.0", 42);
675            let mut replay = Replay::new(header);
676
677            replay.add_checkpoint(StateCheckpoint::new(60, "hash1"));
678            replay.add_checkpoint(StateCheckpoint::new(120, "hash2"));
679
680            assert_eq!(replay.checkpoints.len(), 2);
681        }
682
683        #[test]
684        fn test_checkpoint_at_or_before() {
685            let header = ReplayHeader::new("game", "1.0", 42);
686            let mut replay = Replay::new(header);
687
688            replay.add_checkpoint(StateCheckpoint::new(60, "hash1"));
689            replay.add_checkpoint(StateCheckpoint::new(120, "hash2"));
690
691            let cp = replay.checkpoint_at_or_before(100);
692            assert!(cp.is_some());
693            assert_eq!(cp.unwrap().frame, 60);
694
695            let cp = replay.checkpoint_at_or_before(120);
696            assert!(cp.is_some());
697            assert_eq!(cp.unwrap().frame, 120);
698
699            let cp = replay.checkpoint_at_or_before(50);
700            assert!(cp.is_none());
701        }
702
703        #[test]
704        fn test_metadata() {
705            let header = ReplayHeader::new("game", "1.0", 42);
706            let mut replay = Replay::new(header);
707
708            replay.set_metadata("player", "Alice");
709            replay.set_metadata("difficulty", "hard");
710
711            assert_eq!(replay.metadata.get("player"), Some(&"Alice".to_string()));
712        }
713
714        #[test]
715        fn test_compute_checksum() {
716            let header = ReplayHeader::new("game", "1.0", 42);
717            let mut replay1 = Replay::new(header.clone());
718            let mut replay2 = Replay::new(header);
719
720            replay1.add_input(0, InputEvent::key_press("A"));
721            replay2.add_input(0, InputEvent::key_press("A"));
722
723            assert_eq!(replay1.compute_checksum(), replay2.compute_checksum());
724
725            replay2.add_input(1, InputEvent::key_press("B"));
726            assert_ne!(replay1.compute_checksum(), replay2.compute_checksum());
727        }
728
729        #[test]
730        fn test_finalize_and_verify() {
731            let header = ReplayHeader::new("game", "1.0", 42);
732            let mut replay = Replay::new(header);
733            replay.add_input(0, InputEvent::key_press("A"));
734            replay.finalize();
735
736            assert!(!replay.header.checksum.is_empty());
737            assert!(replay.verify_checksum());
738        }
739    }
740
741    mod replay_recorder_tests {
742        use super::*;
743
744        #[test]
745        fn test_new() {
746            let recorder = ReplayRecorder::new("game", "1.0", 42);
747            assert_eq!(recorder.current_frame(), 0);
748            assert!(recorder.is_recording());
749        }
750
751        #[test]
752        fn test_record_input() {
753            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
754            recorder.record_input(InputEvent::key_press("A"));
755            recorder.next_frame(None);
756            recorder.record_input(InputEvent::key_press("B"));
757
758            let replay = recorder.finalize();
759            assert_eq!(replay.inputs.len(), 2);
760        }
761
762        #[test]
763        fn test_checkpoint() {
764            let mut recorder = ReplayRecorder::new("game", "1.0", 42).with_checkpoint_interval(10);
765
766            for i in 0..25 {
767                recorder.next_frame(Some(&format!("hash{}", i)));
768            }
769
770            let replay = recorder.finalize();
771            // Checkpoints at frames 10, 20
772            assert_eq!(replay.checkpoints.len(), 2);
773        }
774
775        #[test]
776        fn test_stop_recording() {
777            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
778            recorder.record_input(InputEvent::key_press("A"));
779            recorder.stop();
780            recorder.record_input(InputEvent::key_press("B"));
781
782            let replay = recorder.finalize();
783            assert_eq!(replay.inputs.len(), 1); // Only A recorded before stop
784        }
785    }
786
787    mod replay_player_tests {
788        use super::*;
789
790        fn create_test_replay() -> Replay {
791            let header = ReplayHeader::new("game", "1.0", 42);
792            let mut replay = Replay::new(header);
793
794            replay.add_input(0, InputEvent::key_press("A"));
795            replay.add_input(5, InputEvent::key_press("B"));
796            replay.add_input(5, InputEvent::key_press("C"));
797            replay.add_input(10, InputEvent::key_press("D"));
798            replay.add_checkpoint(StateCheckpoint::new(5, "hash5"));
799            replay.header.total_frames = 15;
800            replay
801        }
802
803        #[test]
804        fn test_new() {
805            let replay = create_test_replay();
806            let player = ReplayPlayer::new(replay);
807
808            assert_eq!(player.current_frame(), 0);
809            assert!(player.is_playing());
810        }
811
812        #[test]
813        fn test_get_frame_inputs() {
814            let replay = create_test_replay();
815            let mut player = ReplayPlayer::new(replay);
816
817            // Frame 0: has input A
818            let inputs = player.get_frame_inputs();
819            assert_eq!(inputs.len(), 1);
820            assert_eq!(player.current_frame(), 1);
821
822            // Frames 1-4: no inputs
823            for _ in 1..5 {
824                let inputs = player.get_frame_inputs();
825                assert!(inputs.is_empty());
826            }
827
828            // Frame 5: has inputs B and C
829            let inputs = player.get_frame_inputs();
830            assert_eq!(inputs.len(), 2);
831        }
832
833        #[test]
834        fn test_progress() {
835            let replay = create_test_replay();
836            let mut player = ReplayPlayer::new(replay);
837
838            assert!((player.progress() - 0.0).abs() < f64::EPSILON);
839
840            for _ in 0..7 {
841                let _ = player.get_frame_inputs();
842            }
843
844            // 7/15 ≈ 0.467
845            assert!((player.progress() - 7.0 / 15.0).abs() < 0.01);
846        }
847
848        #[test]
849        fn test_seek() {
850            let replay = create_test_replay();
851            let mut player = ReplayPlayer::new(replay);
852
853            player.seek(10);
854            assert_eq!(player.current_frame(), 10);
855
856            // Should still get input at frame 10
857            let inputs = player.get_frame_inputs();
858            assert_eq!(inputs.len(), 1);
859        }
860
861        #[test]
862        fn test_pause_resume() {
863            let replay = create_test_replay();
864            let mut player = ReplayPlayer::new(replay);
865
866            player.pause();
867            assert!(!player.is_playing());
868
869            let inputs = player.get_frame_inputs();
870            assert!(inputs.is_empty());
871            assert_eq!(player.current_frame(), 0); // Didn't advance
872
873            player.resume();
874            assert!(player.is_playing());
875        }
876
877        #[test]
878        fn test_playback_completion() {
879            let replay = create_test_replay();
880            let mut player = ReplayPlayer::new(replay);
881
882            // Play through entire replay
883            while player.is_playing() {
884                let _ = player.get_frame_inputs();
885            }
886
887            assert_eq!(player.current_frame(), 15);
888            assert!(!player.is_playing());
889        }
890
891        #[test]
892        fn test_verify_state_pass() {
893            let replay = create_test_replay();
894            let mut player = ReplayPlayer::new(replay);
895
896            // Advance to frame 5 (checkpoint is at frame 5)
897            for _ in 0..6 {
898                let _ = player.get_frame_inputs();
899            }
900
901            // Should verify successfully
902            assert!(player.verify_state("hash5").is_ok());
903        }
904
905        #[test]
906        fn test_verify_state_fail() {
907            let replay = create_test_replay();
908            let mut player = ReplayPlayer::new(replay);
909
910            // Advance to frame 5
911            for _ in 0..6 {
912                let _ = player.get_frame_inputs();
913            }
914
915            // Should fail with wrong hash
916            assert!(player.verify_state("wrong_hash").is_err());
917        }
918    }
919
920    mod file_io_tests {
921        use super::*;
922        use tempfile::TempDir;
923
924        #[test]
925        fn test_save_and_load_yaml() {
926            let temp_dir = TempDir::new().unwrap();
927            let path = temp_dir.path().join("replay.yaml");
928
929            let header = ReplayHeader::new("game", "1.0", 42);
930            let mut replay = Replay::new(header);
931            replay.add_input(0, InputEvent::key_press("A"));
932            replay.finalize();
933
934            replay.save_yaml(&path).unwrap();
935            assert!(path.exists());
936
937            let loaded = Replay::load_yaml(&path).unwrap();
938            assert_eq!(loaded.header.seed, 42);
939            assert_eq!(loaded.inputs.len(), 1);
940            assert!(loaded.verify_checksum());
941        }
942
943        #[test]
944        fn test_save_and_load_json() {
945            let temp_dir = TempDir::new().unwrap();
946            let path = temp_dir.path().join("replay.json");
947
948            let header = ReplayHeader::new("game", "1.0", 42);
949            let mut replay = Replay::new(header);
950            replay.add_input(0, InputEvent::key_press("A"));
951            replay.finalize();
952
953            replay.save_json(&path).unwrap();
954            assert!(path.exists());
955
956            let loaded = Replay::load_json(&path).unwrap();
957            assert_eq!(loaded.header.seed, 42);
958            assert!(loaded.verify_checksum());
959        }
960    }
961
962    mod verification_result_tests {
963        use super::*;
964
965        #[test]
966        fn test_success() {
967            let result = VerificationResult::success(100, 5);
968            assert!(result.passed);
969            assert_eq!(result.frames_verified, 100);
970            assert_eq!(result.checkpoints_verified, 5);
971            assert!(result.divergence_frame.is_none());
972            assert!(result.divergence_details.is_none());
973        }
974
975        #[test]
976        fn test_failure() {
977            let result = VerificationResult::failure(50, "State mismatch");
978            assert!(!result.passed);
979            assert_eq!(result.frames_verified, 50);
980            assert_eq!(result.divergence_frame, Some(50));
981            assert!(result
982                .divergence_details
983                .as_ref()
984                .unwrap()
985                .contains("State mismatch"));
986        }
987    }
988
989    mod additional_replay_tests {
990        use super::*;
991
992        #[test]
993        fn test_replay_header_default_fps() {
994            let header = ReplayHeader::new("game", "1.0", 0);
995            assert_eq!(header.fps, 60);
996        }
997
998        #[test]
999        fn test_replay_header_created_at() {
1000            let header = ReplayHeader::new("game", "1.0", 0);
1001            assert!(header.created_at > 0);
1002        }
1003
1004        #[test]
1005        fn test_replay_header_checksum_empty() {
1006            let header = ReplayHeader::new("game", "1.0", 0);
1007            assert!(header.checksum.is_empty());
1008        }
1009
1010        #[test]
1011        fn test_timed_input_event_types() {
1012            let events = vec![
1013                InputEvent::key_press("A"),
1014                InputEvent::key_release("B"),
1015                InputEvent::mouse_click(100.0, 200.0),
1016            ];
1017
1018            for (i, event) in events.into_iter().enumerate() {
1019                let timed = TimedInput::new(i as u64, event);
1020                assert_eq!(timed.frame, i as u64);
1021            }
1022        }
1023
1024        #[test]
1025        fn test_replay_inputs_at_frame_none() {
1026            let header = ReplayHeader::new("game", "1.0", 0);
1027            let replay = Replay::new(header);
1028            let inputs = replay.inputs_at_frame(0);
1029            assert!(inputs.is_empty());
1030        }
1031
1032        #[test]
1033        fn test_replay_checkpoint_none() {
1034            let header = ReplayHeader::new("game", "1.0", 0);
1035            let replay = Replay::new(header);
1036            assert!(replay.checkpoint_at_or_before(0).is_none());
1037        }
1038
1039        #[test]
1040        fn test_replay_verify_checksum_mismatch() {
1041            let header = ReplayHeader::new("game", "1.0", 42);
1042            let mut replay = Replay::new(header);
1043            replay.add_input(0, InputEvent::key_press("A"));
1044            replay.finalize();
1045
1046            // Tamper with checksum
1047            replay.header.checksum = "invalid".to_string();
1048            assert!(!replay.verify_checksum());
1049        }
1050    }
1051
1052    mod additional_recorder_tests {
1053        use super::*;
1054
1055        #[test]
1056        fn test_recorder_record_inputs() {
1057            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1058            let events = vec![
1059                InputEvent::key_press("A"),
1060                InputEvent::key_press("B"),
1061                InputEvent::key_press("C"),
1062            ];
1063            recorder.record_inputs(&events);
1064
1065            let replay = recorder.finalize();
1066            assert_eq!(replay.inputs.len(), 3);
1067        }
1068
1069        #[test]
1070        fn test_recorder_checkpoint() {
1071            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1072            recorder.checkpoint("hash_at_0");
1073            recorder.next_frame(None);
1074            recorder.checkpoint("hash_at_1");
1075
1076            let replay = recorder.finalize();
1077            assert_eq!(replay.checkpoints.len(), 2);
1078        }
1079
1080        #[test]
1081        fn test_recorder_checkpoint_with_data() {
1082            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1083            let mut data = HashMap::new();
1084            data.insert("score".to_string(), serde_json::json!(100));
1085            data.insert("level".to_string(), serde_json::json!(5));
1086            recorder.checkpoint_with_data("hash", data);
1087
1088            let replay = recorder.finalize();
1089            assert_eq!(replay.checkpoints.len(), 1);
1090            assert!(replay.checkpoints[0].state_data.is_some());
1091        }
1092
1093        #[test]
1094        fn test_recorder_with_fps() {
1095            let recorder = ReplayRecorder::new("game", "1.0", 42).with_fps(30);
1096            let replay = recorder.finalize();
1097            assert_eq!(replay.header.fps, 30);
1098        }
1099
1100        #[test]
1101        fn test_recorder_not_recording() {
1102            let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1103            recorder.stop();
1104            assert!(!recorder.is_recording());
1105
1106            // Should not record after stop
1107            recorder.record_input(InputEvent::key_press("A"));
1108            let replay = recorder.finalize();
1109            assert!(replay.inputs.is_empty());
1110        }
1111    }
1112
1113    mod additional_player_tests {
1114        use super::*;
1115
1116        fn create_simple_replay(total_frames: u64) -> Replay {
1117            let header = ReplayHeader::new("game", "1.0", 42);
1118            let mut replay = Replay::new(header);
1119            replay.header.total_frames = total_frames;
1120            replay
1121        }
1122
1123        #[test]
1124        fn test_player_with_speed() {
1125            let replay = create_simple_replay(100);
1126            let player = ReplayPlayer::new(replay).with_speed(2.0);
1127            assert!((player.speed - 2.0).abs() < f64::EPSILON);
1128        }
1129
1130        #[test]
1131        fn test_player_total_frames() {
1132            let replay = create_simple_replay(100);
1133            let player = ReplayPlayer::new(replay);
1134            assert_eq!(player.total_frames(), 100);
1135        }
1136
1137        #[test]
1138        fn test_player_progress_zero_frames() {
1139            let replay = create_simple_replay(0);
1140            let player = ReplayPlayer::new(replay);
1141            assert!((player.progress() - 1.0).abs() < f64::EPSILON);
1142        }
1143
1144        #[test]
1145        fn test_player_seek_beyond_total() {
1146            let replay = create_simple_replay(50);
1147            let mut player = ReplayPlayer::new(replay);
1148            player.seek(100);
1149            assert_eq!(player.current_frame(), 50);
1150            assert!(!player.is_playing());
1151        }
1152
1153        #[test]
1154        fn test_player_resume_at_end() {
1155            let replay = create_simple_replay(10);
1156            let mut player = ReplayPlayer::new(replay);
1157
1158            // Play to end
1159            while player.is_playing() {
1160                let _ = player.get_frame_inputs();
1161            }
1162
1163            player.resume();
1164            // Should still not be playing since we're at the end
1165            assert!(!player.is_playing());
1166        }
1167
1168        #[test]
1169        fn test_player_replay_accessor() {
1170            let replay = create_simple_replay(50);
1171            let player = ReplayPlayer::new(replay);
1172            let accessed_replay = player.replay();
1173            assert_eq!(accessed_replay.header.total_frames, 50);
1174        }
1175
1176        #[test]
1177        fn test_player_expected_checkpoint_none() {
1178            let replay = create_simple_replay(50);
1179            let player = ReplayPlayer::new(replay);
1180            // At frame 0, previous frame would be -1, no checkpoint
1181            assert!(player.expected_checkpoint().is_none());
1182        }
1183
1184        #[test]
1185        fn test_player_verify_state_no_checkpoint() {
1186            let replay = create_simple_replay(50);
1187            let player = ReplayPlayer::new(replay);
1188            // Should be Ok since there's no checkpoint to verify against
1189            assert!(player.verify_state("any_hash").is_ok());
1190        }
1191
1192        #[test]
1193        fn test_player_inputs_with_gaps() {
1194            let header = ReplayHeader::new("game", "1.0", 42);
1195            let mut replay = Replay::new(header);
1196
1197            // Inputs at frames 0, 10, 20
1198            replay.add_input(0, InputEvent::key_press("A"));
1199            replay.add_input(10, InputEvent::key_press("B"));
1200            replay.add_input(20, InputEvent::key_press("C"));
1201            replay.header.total_frames = 25;
1202
1203            let mut player = ReplayPlayer::new(replay);
1204
1205            // Frame 0: has A
1206            let inputs = player.get_frame_inputs();
1207            assert_eq!(inputs.len(), 1);
1208
1209            // Frames 1-9: no inputs
1210            for _ in 1..10 {
1211                let inputs = player.get_frame_inputs();
1212                assert!(inputs.is_empty());
1213            }
1214
1215            // Frame 10: has B
1216            let inputs = player.get_frame_inputs();
1217            assert_eq!(inputs.len(), 1);
1218        }
1219
1220        #[test]
1221        fn test_player_seek_updates_input_index() {
1222            let header = ReplayHeader::new("game", "1.0", 42);
1223            let mut replay = Replay::new(header);
1224
1225            replay.add_input(0, InputEvent::key_press("A"));
1226            replay.add_input(5, InputEvent::key_press("B"));
1227            replay.add_input(10, InputEvent::key_press("C"));
1228            replay.header.total_frames = 15;
1229
1230            let mut player = ReplayPlayer::new(replay);
1231
1232            // Seek to frame 7 (between B and C)
1233            player.seek(7);
1234
1235            // Advance, should get no input
1236            let inputs = player.get_frame_inputs();
1237            assert!(inputs.is_empty());
1238
1239            // Continue to frame 10, should get C
1240            let _ = player.get_frame_inputs(); // frame 8
1241            let _ = player.get_frame_inputs(); // frame 9
1242            let inputs = player.get_frame_inputs(); // frame 10
1243            assert_eq!(inputs.len(), 1);
1244        }
1245    }
1246
1247    mod additional_file_io_tests {
1248        use super::*;
1249        use tempfile::TempDir;
1250
1251        #[test]
1252        fn test_save_yaml_creates_parent_dirs() {
1253            let temp_dir = TempDir::new().unwrap();
1254            let path = temp_dir
1255                .path()
1256                .join("subdir")
1257                .join("deep")
1258                .join("replay.yaml");
1259
1260            let header = ReplayHeader::new("game", "1.0", 42);
1261            let replay = Replay::new(header);
1262
1263            replay.save_yaml(&path).unwrap();
1264            assert!(path.exists());
1265        }
1266
1267        #[test]
1268        fn test_save_json_creates_parent_dirs() {
1269            let temp_dir = TempDir::new().unwrap();
1270            let path = temp_dir
1271                .path()
1272                .join("subdir")
1273                .join("deep")
1274                .join("replay.json");
1275
1276            let header = ReplayHeader::new("game", "1.0", 42);
1277            let replay = Replay::new(header);
1278
1279            replay.save_json(&path).unwrap();
1280            assert!(path.exists());
1281        }
1282
1283        #[test]
1284        fn test_load_yaml_not_found() {
1285            let result = Replay::load_yaml(Path::new("/nonexistent/replay.yaml"));
1286            assert!(result.is_err());
1287        }
1288
1289        #[test]
1290        fn test_load_json_not_found() {
1291            let result = Replay::load_json(Path::new("/nonexistent/replay.json"));
1292            assert!(result.is_err());
1293        }
1294
1295        #[test]
1296        fn test_load_yaml_invalid() {
1297            let temp_dir = TempDir::new().unwrap();
1298            let path = temp_dir.path().join("invalid.yaml");
1299            std::fs::write(&path, "not: valid: yaml: {{{").unwrap();
1300
1301            let result = Replay::load_yaml(&path);
1302            assert!(result.is_err());
1303        }
1304
1305        #[test]
1306        fn test_load_json_invalid() {
1307            let temp_dir = TempDir::new().unwrap();
1308            let path = temp_dir.path().join("invalid.json");
1309            std::fs::write(&path, "not valid json {{{").unwrap();
1310
1311            let result = Replay::load_json(&path);
1312            assert!(result.is_err());
1313        }
1314
1315        #[test]
1316        fn test_replay_with_metadata_yaml() {
1317            let temp_dir = TempDir::new().unwrap();
1318            let path = temp_dir.path().join("replay.yaml");
1319
1320            let header = ReplayHeader::new("game", "1.0", 42);
1321            let mut replay = Replay::new(header);
1322            replay.set_metadata("player", "Alice");
1323            replay.set_metadata("score", "1000");
1324            replay.finalize();
1325
1326            replay.save_yaml(&path).unwrap();
1327            let loaded = Replay::load_yaml(&path).unwrap();
1328
1329            assert_eq!(loaded.metadata.get("player"), Some(&"Alice".to_string()));
1330            assert_eq!(loaded.metadata.get("score"), Some(&"1000".to_string()));
1331        }
1332
1333        #[test]
1334        fn test_replay_with_checkpoints_json() {
1335            let temp_dir = TempDir::new().unwrap();
1336            let path = temp_dir.path().join("replay.json");
1337
1338            let header = ReplayHeader::new("game", "1.0", 42);
1339            let mut replay = Replay::new(header);
1340
1341            let mut data = HashMap::new();
1342            data.insert("level".to_string(), serde_json::json!(3));
1343            replay.add_checkpoint(StateCheckpoint::with_data(60, "hash1", data));
1344
1345            replay.finalize();
1346            replay.save_json(&path).unwrap();
1347
1348            let loaded = Replay::load_json(&path).unwrap();
1349            assert_eq!(loaded.checkpoints.len(), 1);
1350            assert!(loaded.checkpoints[0].state_data.is_some());
1351        }
1352    }
1353
1354    mod additional_edge_case_tests {
1355        use super::*;
1356
1357        #[test]
1358        fn test_replay_format_version() {
1359            assert_eq!(REPLAY_FORMAT_VERSION, 1);
1360        }
1361
1362        #[test]
1363        fn test_state_checkpoint_without_data() {
1364            let cp = StateCheckpoint::new(100, "hash");
1365            assert!(cp.state_data.is_none());
1366            assert_eq!(cp.frame, 100);
1367            assert_eq!(cp.state_hash, "hash");
1368        }
1369
1370        #[test]
1371        fn test_replay_checksum_with_checkpoints() {
1372            let header = ReplayHeader::new("game", "1.0", 42);
1373            let mut replay = Replay::new(header);
1374
1375            replay.add_checkpoint(StateCheckpoint::new(10, "hash1"));
1376            replay.add_checkpoint(StateCheckpoint::new(20, "hash2"));
1377
1378            let checksum1 = replay.compute_checksum();
1379
1380            replay.add_checkpoint(StateCheckpoint::new(30, "hash3"));
1381            let checksum2 = replay.compute_checksum();
1382
1383            assert_ne!(checksum1, checksum2);
1384        }
1385
1386        #[test]
1387        fn test_replay_total_frames_from_checkpoint() {
1388            let header = ReplayHeader::new("game", "1.0", 42);
1389            let mut replay = Replay::new(header);
1390
1391            assert_eq!(replay.header.total_frames, 0);
1392
1393            replay.add_checkpoint(StateCheckpoint::new(100, "hash"));
1394            assert_eq!(replay.header.total_frames, 101);
1395        }
1396
1397        #[test]
1398        fn test_replay_total_frames_from_input() {
1399            let header = ReplayHeader::new("game", "1.0", 42);
1400            let mut replay = Replay::new(header);
1401
1402            replay.add_input(50, InputEvent::key_press("A"));
1403            assert_eq!(replay.header.total_frames, 51);
1404        }
1405
1406        #[test]
1407        fn test_player_skipped_inputs() {
1408            let header = ReplayHeader::new("game", "1.0", 42);
1409            let mut replay = Replay::new(header);
1410
1411            // Add input at frame 0
1412            replay.add_input(0, InputEvent::key_press("A"));
1413            replay.header.total_frames = 5;
1414
1415            let mut player = ReplayPlayer::new(replay);
1416
1417            // Seek past frame 0
1418            player.seek(3);
1419
1420            // Input at frame 0 should be skipped
1421            let inputs = player.get_frame_inputs();
1422            assert!(inputs.is_empty());
1423        }
1424    }
1425}