Skip to main content

jugar_probar/
bridge.rs

1//! StateBridge - Game State Inspection Bridge
2//!
3//! Per spec Section 3.3: Bridge between browser and game state with zero-copy views.
4//!
5//! # Architecture
6//!
7//! ```text
8//! ┌────────────────────────────────────────────────────────────────────┐
9//! │  StateBridge                                                        │
10//! │  ──────────────────                                                │
11//! │                                                                    │
12//! │  ┌─────────────────────┐     ┌─────────────────────────┐          │
13//! │  │  Zero-Copy Path     │     │  Serialized Path        │          │
14//! │  │  (WasmRuntime)      │     │  (BrowserController)    │          │
15//! │  │                     │     │                         │          │
16//! │  │  • MemoryView       │     │  • bincode RPC          │          │
17//! │  │  • Direct access    │     │  • Snapshot cache       │          │
18//! │  │  • < 100ns reads    │     │  • Delta encoding       │          │
19//! │  └─────────────────────┘     └─────────────────────────┘          │
20//! │                                                                    │
21//! │  Toyota Principle: Muda elimination via zero-copy where possible  │
22//! └────────────────────────────────────────────────────────────────────┘
23//! ```
24//!
25//! # Toyota Principles Applied
26//!
27//! - **Muda (Waste Elimination)**: Zero-copy memory views avoid serialization
28//! - **Poka-Yoke (Error Proofing)**: Type-safe entity queries
29
30use crate::result::ProbarResult;
31use crate::runtime::{EntityId, MemoryView, StateDelta};
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35/// Game state snapshot with delta encoding
36///
37/// Per spec: Delta encoding achieves 94% overhead reduction (Lavoie \[9\])
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct GameStateSnapshot {
40    /// Frame number when snapshot was taken
41    pub frame: u64,
42    /// Game state data
43    pub state: GameStateData,
44    /// Perceptual hash for visual comparison (more robust than SHA-256)
45    pub visual_phash: u64,
46    /// Cryptographic hash for determinism verification
47    pub state_hash: u64,
48}
49
50impl GameStateSnapshot {
51    /// Create a new snapshot
52    #[must_use]
53    pub fn new(frame: u64, state: GameStateData) -> Self {
54        let state_hash = state.compute_hash();
55        Self {
56            frame,
57            state,
58            visual_phash: 0,
59            state_hash,
60        }
61    }
62
63    /// Set the perceptual hash
64    #[must_use]
65    pub const fn with_phash(mut self, phash: u64) -> Self {
66        self.visual_phash = phash;
67        self
68    }
69}
70
71/// Game state data container
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct GameStateData {
74    /// Entity positions (entity_id -> (x, y))
75    pub positions: HashMap<u32, (f32, f32)>,
76    /// Entity velocities (entity_id -> (vx, vy))
77    pub velocities: HashMap<u32, (f32, f32)>,
78    /// Scores
79    pub scores: HashMap<String, i32>,
80    /// Game flags
81    pub flags: HashMap<String, bool>,
82    /// Custom state values
83    pub custom: HashMap<String, serde_json::Value>,
84}
85
86impl GameStateData {
87    /// Create empty state data
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Add entity position
94    pub fn add_position(&mut self, entity_id: u32, x: f32, y: f32) {
95        self.positions.insert(entity_id, (x, y));
96    }
97
98    /// Add entity velocity
99    pub fn add_velocity(&mut self, entity_id: u32, vx: f32, vy: f32) {
100        self.velocities.insert(entity_id, (vx, vy));
101    }
102
103    /// Set a score
104    pub fn set_score(&mut self, name: impl Into<String>, value: i32) {
105        self.scores.insert(name.into(), value);
106    }
107
108    /// Set a flag
109    pub fn set_flag(&mut self, name: impl Into<String>, value: bool) {
110        self.flags.insert(name.into(), value);
111    }
112
113    /// Get entity position
114    #[must_use]
115    pub fn get_position(&self, entity_id: u32) -> Option<(f32, f32)> {
116        self.positions.get(&entity_id).copied()
117    }
118
119    /// Get entity velocity
120    #[must_use]
121    pub fn get_velocity(&self, entity_id: u32) -> Option<(f32, f32)> {
122        self.velocities.get(&entity_id).copied()
123    }
124
125    /// Get a score
126    #[must_use]
127    pub fn get_score(&self, name: &str) -> Option<i32> {
128        self.scores.get(name).copied()
129    }
130
131    /// Get a flag
132    #[must_use]
133    pub fn get_flag(&self, name: &str) -> Option<bool> {
134        self.flags.get(name).copied()
135    }
136
137    /// Compute hash of the state
138    #[must_use]
139    pub fn compute_hash(&self) -> u64 {
140        use std::collections::hash_map::DefaultHasher;
141        use std::hash::{Hash, Hasher};
142
143        let mut hasher = DefaultHasher::new();
144
145        // Hash positions in sorted order for determinism
146        let mut positions: Vec<_> = self.positions.iter().collect();
147        positions.sort_by_key(|(k, _)| *k);
148        for (k, (x, y)) in positions {
149            k.hash(&mut hasher);
150            x.to_bits().hash(&mut hasher);
151            y.to_bits().hash(&mut hasher);
152        }
153
154        // Hash velocities
155        let mut velocities: Vec<_> = self.velocities.iter().collect();
156        velocities.sort_by_key(|(k, _)| *k);
157        for (k, (vx, vy)) in velocities {
158            k.hash(&mut hasher);
159            vx.to_bits().hash(&mut hasher);
160            vy.to_bits().hash(&mut hasher);
161        }
162
163        // Hash scores
164        let mut scores: Vec<_> = self.scores.iter().collect();
165        scores.sort_by_key(|(k, _)| k.as_str());
166        for (k, v) in scores {
167            k.hash(&mut hasher);
168            v.hash(&mut hasher);
169        }
170
171        // Hash flags
172        let mut flags: Vec<_> = self.flags.iter().collect();
173        flags.sort_by_key(|(k, _)| k.as_str());
174        for (k, v) in flags {
175            k.hash(&mut hasher);
176            v.hash(&mut hasher);
177        }
178
179        hasher.finish()
180    }
181}
182
183/// Entity snapshot for inspection
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EntitySnapshot {
186    /// Entity ID
187    pub id: EntityId,
188    /// Entity name (for debugging)
189    pub name: String,
190    /// Position (x, y)
191    pub position: Option<(f32, f32)>,
192    /// Velocity (vx, vy)
193    pub velocity: Option<(f32, f32)>,
194    /// Is entity active
195    pub active: bool,
196    /// Custom components as JSON
197    pub components: HashMap<String, serde_json::Value>,
198}
199
200impl EntitySnapshot {
201    /// Create a new entity snapshot
202    #[must_use]
203    pub fn new(id: EntityId, name: impl Into<String>) -> Self {
204        Self {
205            id,
206            name: name.into(),
207            position: None,
208            velocity: None,
209            active: true,
210            components: HashMap::new(),
211        }
212    }
213
214    /// Set position
215    #[must_use]
216    pub const fn with_position(mut self, x: f32, y: f32) -> Self {
217        self.position = Some((x, y));
218        self
219    }
220
221    /// Set velocity
222    #[must_use]
223    pub const fn with_velocity(mut self, vx: f32, vy: f32) -> Self {
224        self.velocity = Some((vx, vy));
225        self
226    }
227
228    /// Add a component
229    pub fn add_component(&mut self, name: impl Into<String>, value: serde_json::Value) {
230        self.components.insert(name.into(), value);
231    }
232}
233
234/// Visual comparison result using perceptual hash
235///
236/// Per Shamir \[19\]: pHash more robust than SHA-256 for game frame comparison
237#[derive(Debug, Clone)]
238pub struct VisualDiff {
239    /// Perceptual similarity (0.0 to 1.0)
240    pub perceptual_similarity: f64,
241    /// Pixel-level difference count
242    pub pixel_diff_count: u64,
243    /// Highlighted regions that differ
244    pub diff_regions: Vec<DiffRegion>,
245    /// Expected image data (if available)
246    pub expected: Option<Vec<u8>>,
247    /// Actual image data
248    pub actual: Vec<u8>,
249    /// Diff overlay image
250    pub highlighted: Vec<u8>,
251}
252
253impl VisualDiff {
254    /// Create a new visual diff
255    #[must_use]
256    pub fn new(similarity: f64, actual: Vec<u8>) -> Self {
257        Self {
258            perceptual_similarity: similarity,
259            pixel_diff_count: 0,
260            diff_regions: Vec::new(),
261            expected: None,
262            actual,
263            highlighted: Vec::new(),
264        }
265    }
266
267    /// Check if images match within threshold
268    #[must_use]
269    pub fn matches(&self, threshold: f64) -> bool {
270        self.perceptual_similarity >= threshold
271    }
272
273    /// Check if images are identical
274    #[must_use]
275    pub fn is_identical(&self) -> bool {
276        self.perceptual_similarity >= 1.0 - f64::EPSILON
277    }
278}
279
280/// Region where images differ
281#[derive(Debug, Clone)]
282pub struct DiffRegion {
283    /// X position
284    pub x: u32,
285    /// Y position
286    pub y: u32,
287    /// Width
288    pub width: u32,
289    /// Height
290    pub height: u32,
291    /// Difference intensity (0.0 to 1.0)
292    pub intensity: f64,
293}
294
295/// Bridge connection type
296#[derive(Debug, Clone)]
297pub enum BridgeConnection {
298    /// Direct memory access (WasmRuntime)
299    Direct,
300    /// RPC via browser (BrowserController)
301    Rpc {
302        /// Session ID
303        session_id: String,
304    },
305}
306
307/// LRU cache for snapshot storage
308#[derive(Debug)]
309pub struct SnapshotCache {
310    /// Maximum cache size
311    max_size: usize,
312    /// Cached snapshots (frame -> snapshot)
313    cache: HashMap<u64, GameStateSnapshot>,
314    /// Access order for LRU eviction
315    access_order: Vec<u64>,
316}
317
318impl SnapshotCache {
319    /// Create new cache with given size
320    #[must_use]
321    pub fn new(max_size: usize) -> Self {
322        Self {
323            max_size,
324            cache: HashMap::new(),
325            access_order: Vec::new(),
326        }
327    }
328
329    /// Get snapshot from cache
330    pub fn get(&mut self, frame: u64) -> Option<&GameStateSnapshot> {
331        if self.cache.contains_key(&frame) {
332            // Update access order
333            self.access_order.retain(|&f| f != frame);
334            self.access_order.push(frame);
335            self.cache.get(&frame)
336        } else {
337            None
338        }
339    }
340
341    /// Insert snapshot into cache
342    pub fn insert(&mut self, frame: u64, snapshot: GameStateSnapshot) {
343        // Evict if at capacity
344        while self.cache.len() >= self.max_size {
345            if let Some(oldest) = self.access_order.first().copied() {
346                self.cache.remove(&oldest);
347                self.access_order.remove(0);
348            } else {
349                break;
350            }
351        }
352
353        self.cache.insert(frame, snapshot);
354        self.access_order.push(frame);
355    }
356
357    /// Clear the cache
358    pub fn clear(&mut self) {
359        self.cache.clear();
360        self.access_order.clear();
361    }
362
363    /// Get cache size
364    #[must_use]
365    pub fn len(&self) -> usize {
366        self.cache.len()
367    }
368
369    /// Check if cache is empty
370    #[must_use]
371    pub fn is_empty(&self) -> bool {
372        self.cache.is_empty()
373    }
374}
375
376/// State bridge for game state inspection
377///
378/// Provides unified access to game state whether using WasmRuntime (zero-copy)
379/// or BrowserController (serialized RPC).
380#[derive(Debug)]
381pub struct StateBridge {
382    /// Connection type
383    connection: BridgeConnection,
384    /// Memory view for zero-copy access (used in direct mode)
385    memory_view: Option<MemoryView>,
386    /// Snapshot cache for RPC mode
387    snapshot_cache: SnapshotCache,
388    /// Delta history for replay
389    delta_history: Vec<StateDelta>,
390}
391
392impl StateBridge {
393    /// Create bridge with direct memory access (for WasmRuntime)
394    #[must_use]
395    pub fn direct(memory_view: MemoryView) -> Self {
396        Self {
397            connection: BridgeConnection::Direct,
398            memory_view: Some(memory_view),
399            snapshot_cache: SnapshotCache::new(100),
400            delta_history: Vec::new(),
401        }
402    }
403
404    /// Create bridge with RPC connection (for BrowserController)
405    #[must_use]
406    pub fn rpc(session_id: impl Into<String>) -> Self {
407        Self {
408            connection: BridgeConnection::Rpc {
409                session_id: session_id.into(),
410            },
411            memory_view: None,
412            snapshot_cache: SnapshotCache::new(100),
413            delta_history: Vec::new(),
414        }
415    }
416
417    /// Check if using direct memory access
418    #[must_use]
419    pub const fn is_direct(&self) -> bool {
420        matches!(self.connection, BridgeConnection::Direct)
421    }
422
423    /// Get the memory view if using direct mode
424    #[must_use]
425    pub const fn memory_view(&self) -> Option<&MemoryView> {
426        self.memory_view.as_ref()
427    }
428
429    /// Query entity by ID
430    ///
431    /// # Errors
432    ///
433    /// Returns error if entity not found
434    pub fn query_entity(&self, entity_id: EntityId) -> ProbarResult<EntitySnapshot> {
435        match &self.connection {
436            BridgeConnection::Direct => {
437                // In a real implementation, this would read from MemoryView
438                // For now, return a mock entity
439                Ok(EntitySnapshot::new(
440                    entity_id,
441                    format!("entity_{}", entity_id.raw()),
442                ))
443            }
444            BridgeConnection::Rpc { session_id } => {
445                // In a real implementation, this would make an RPC call
446                let _ = session_id;
447                Ok(EntitySnapshot::new(
448                    entity_id,
449                    format!("entity_{}", entity_id.raw()),
450                ))
451            }
452        }
453    }
454
455    /// Get current game state snapshot
456    ///
457    /// # Errors
458    ///
459    /// Returns error if state cannot be captured
460    pub fn snapshot(&mut self, frame: u64) -> ProbarResult<GameStateSnapshot> {
461        // Check cache first
462        if let Some(cached) = self.snapshot_cache.get(frame) {
463            return Ok(cached.clone());
464        }
465
466        // Create new snapshot
467        let state = GameStateData::new();
468        let snapshot = GameStateSnapshot::new(frame, state);
469
470        // Cache it
471        self.snapshot_cache.insert(frame, snapshot.clone());
472
473        Ok(snapshot)
474    }
475
476    /// Record a delta from current state
477    pub fn record_delta(&mut self, delta: StateDelta) {
478        self.delta_history.push(delta);
479    }
480
481    /// Get delta history
482    #[must_use]
483    pub fn deltas(&self) -> &[StateDelta] {
484        &self.delta_history
485    }
486
487    /// Clear delta history
488    pub fn clear_deltas(&mut self) {
489        self.delta_history.clear();
490    }
491
492    /// Compute perceptual hash for image
493    ///
494    /// Per Shamir \[19\]: pHash is more robust than pixel comparison
495    #[must_use]
496    pub fn compute_phash(image_data: &[u8]) -> u64 {
497        // Simplified pHash implementation
498        // In production, use proper DCT-based algorithm
499
500        use std::collections::hash_map::DefaultHasher;
501        use std::hash::{Hash, Hasher};
502
503        let mut hasher = DefaultHasher::new();
504
505        // Sample every Nth byte for speed
506        let sample_rate = (image_data.len() / 64).max(1);
507        for (i, &byte) in image_data.iter().enumerate() {
508            if i % sample_rate == 0 {
509                byte.hash(&mut hasher);
510            }
511        }
512
513        hasher.finish()
514    }
515
516    /// Compare two images using perceptual hash
517    #[must_use]
518    pub fn visual_compare(expected: &[u8], actual: &[u8]) -> VisualDiff {
519        let phash_expected = Self::compute_phash(expected);
520        let phash_actual = Self::compute_phash(actual);
521
522        // Hamming distance between hashes
523        let hamming_distance = (phash_expected ^ phash_actual).count_ones();
524        let similarity = 1.0 - (f64::from(hamming_distance) / 64.0);
525
526        VisualDiff {
527            perceptual_similarity: similarity,
528            pixel_diff_count: 0, // Would need actual pixel comparison
529            diff_regions: Vec::new(),
530            expected: Some(expected.to_vec()),
531            actual: actual.to_vec(),
532            highlighted: Vec::new(),
533        }
534    }
535}
536
537// ============================================================================
538// EXTREME TDD: Tests written FIRST per spec Section 6.1
539// ============================================================================
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used, clippy::expect_used)]
543mod tests {
544    use super::*;
545
546    mod game_state_data_tests {
547        use super::*;
548
549        #[test]
550        fn test_new_state_data() {
551            let state = GameStateData::new();
552            assert!(state.positions.is_empty());
553            assert!(state.velocities.is_empty());
554            assert!(state.scores.is_empty());
555        }
556
557        #[test]
558        fn test_add_position() {
559            let mut state = GameStateData::new();
560            state.add_position(1, 100.0, 200.0);
561            assert_eq!(state.get_position(1), Some((100.0, 200.0)));
562            assert_eq!(state.get_position(2), None);
563        }
564
565        #[test]
566        fn test_add_velocity() {
567            let mut state = GameStateData::new();
568            state.add_velocity(1, 5.0, -3.0);
569            assert_eq!(state.get_velocity(1), Some((5.0, -3.0)));
570        }
571
572        #[test]
573        fn test_set_score() {
574            let mut state = GameStateData::new();
575            state.set_score("player1", 100);
576            state.set_score("player2", 50);
577            assert_eq!(state.get_score("player1"), Some(100));
578            assert_eq!(state.get_score("player2"), Some(50));
579            assert_eq!(state.get_score("player3"), None);
580        }
581
582        #[test]
583        fn test_set_flag() {
584            let mut state = GameStateData::new();
585            state.set_flag("game_over", false);
586            state.set_flag("paused", true);
587            assert_eq!(state.get_flag("game_over"), Some(false));
588            assert_eq!(state.get_flag("paused"), Some(true));
589        }
590
591        #[test]
592        fn test_compute_hash_deterministic() {
593            let mut state1 = GameStateData::new();
594            state1.add_position(1, 100.0, 200.0);
595            state1.set_score("player1", 50);
596
597            let mut state2 = GameStateData::new();
598            state2.set_score("player1", 50);
599            state2.add_position(1, 100.0, 200.0);
600
601            // Hash should be same regardless of insertion order
602            assert_eq!(state1.compute_hash(), state2.compute_hash());
603        }
604
605        #[test]
606        fn test_compute_hash_different() {
607            let mut state1 = GameStateData::new();
608            state1.add_position(1, 100.0, 200.0);
609
610            let mut state2 = GameStateData::new();
611            state2.add_position(1, 100.0, 201.0);
612
613            assert_ne!(state1.compute_hash(), state2.compute_hash());
614        }
615    }
616
617    mod entity_snapshot_tests {
618        use super::*;
619
620        #[test]
621        fn test_new_entity_snapshot() {
622            let entity = EntitySnapshot::new(EntityId::new(42), "player");
623            assert_eq!(entity.id.raw(), 42);
624            assert_eq!(entity.name, "player");
625            assert!(entity.active);
626        }
627
628        #[test]
629        fn test_with_position() {
630            let entity = EntitySnapshot::new(EntityId::new(1), "ball").with_position(100.0, 200.0);
631            assert_eq!(entity.position, Some((100.0, 200.0)));
632        }
633
634        #[test]
635        fn test_with_velocity() {
636            let entity = EntitySnapshot::new(EntityId::new(1), "ball").with_velocity(5.0, -3.0);
637            assert_eq!(entity.velocity, Some((5.0, -3.0)));
638        }
639
640        #[test]
641        fn test_add_component() {
642            let mut entity = EntitySnapshot::new(EntityId::new(1), "player");
643            entity.add_component("health", serde_json::json!(100));
644            assert_eq!(
645                entity.components.get("health"),
646                Some(&serde_json::json!(100))
647            );
648        }
649    }
650
651    mod game_state_snapshot_tests {
652        use super::*;
653
654        #[test]
655        fn test_new_snapshot() {
656            let state = GameStateData::new();
657            let snapshot = GameStateSnapshot::new(100, state);
658            assert_eq!(snapshot.frame, 100);
659            assert_ne!(snapshot.state_hash, 0);
660        }
661
662        #[test]
663        fn test_with_phash() {
664            let state = GameStateData::new();
665            let snapshot = GameStateSnapshot::new(0, state).with_phash(12345);
666            assert_eq!(snapshot.visual_phash, 12345);
667        }
668    }
669
670    mod visual_diff_tests {
671        use super::*;
672
673        #[test]
674        fn test_new_visual_diff() {
675            let diff = VisualDiff::new(0.95, vec![1, 2, 3]);
676            assert!((diff.perceptual_similarity - 0.95).abs() < f64::EPSILON);
677        }
678
679        #[test]
680        fn test_matches_threshold() {
681            let diff = VisualDiff::new(0.95, vec![]);
682            assert!(diff.matches(0.90));
683            assert!(diff.matches(0.95));
684            assert!(!diff.matches(0.99));
685        }
686
687        #[test]
688        fn test_is_identical() {
689            let identical = VisualDiff::new(1.0, vec![]);
690            assert!(identical.is_identical());
691
692            let different = VisualDiff::new(0.99, vec![]);
693            assert!(!different.is_identical());
694        }
695    }
696
697    mod snapshot_cache_tests {
698        use super::*;
699
700        #[test]
701        fn test_new_cache() {
702            let cache = SnapshotCache::new(10);
703            assert!(cache.is_empty());
704            assert_eq!(cache.len(), 0);
705        }
706
707        #[test]
708        fn test_insert_and_get() {
709            let mut cache = SnapshotCache::new(10);
710            let snapshot = GameStateSnapshot::new(1, GameStateData::new());
711            cache.insert(1, snapshot);
712
713            assert!(!cache.is_empty());
714            assert_eq!(cache.len(), 1);
715            assert!(cache.get(1).is_some());
716            assert!(cache.get(2).is_none());
717        }
718
719        #[test]
720        fn test_lru_eviction() {
721            let mut cache = SnapshotCache::new(2);
722
723            cache.insert(1, GameStateSnapshot::new(1, GameStateData::new()));
724            cache.insert(2, GameStateSnapshot::new(2, GameStateData::new()));
725
726            // Access frame 1 to make it most recently used
727            let _ = cache.get(1);
728
729            // Insert frame 3, should evict frame 2 (least recently used)
730            cache.insert(3, GameStateSnapshot::new(3, GameStateData::new()));
731
732            assert!(cache.get(1).is_some()); // Still there
733            assert!(cache.get(3).is_some()); // Added
734                                             // Frame 2 was evicted
735        }
736
737        #[test]
738        fn test_clear() {
739            let mut cache = SnapshotCache::new(10);
740            cache.insert(1, GameStateSnapshot::new(1, GameStateData::new()));
741            cache.insert(2, GameStateSnapshot::new(2, GameStateData::new()));
742            cache.clear();
743            assert!(cache.is_empty());
744        }
745    }
746
747    mod state_bridge_tests {
748        use super::*;
749
750        #[test]
751        fn test_direct_bridge() {
752            let view = MemoryView::new(1024);
753            let bridge = StateBridge::direct(view);
754            assert!(bridge.is_direct());
755        }
756
757        #[test]
758        fn test_rpc_bridge() {
759            let bridge = StateBridge::rpc("session-123");
760            assert!(!bridge.is_direct());
761        }
762
763        #[test]
764        fn test_query_entity() {
765            let view = MemoryView::new(1024);
766            let bridge = StateBridge::direct(view);
767            let entity = bridge.query_entity(EntityId::new(42)).unwrap();
768            assert_eq!(entity.id.raw(), 42);
769        }
770
771        #[test]
772        fn test_snapshot_caching() {
773            let view = MemoryView::new(1024);
774            let mut bridge = StateBridge::direct(view);
775
776            let snap1 = bridge.snapshot(100).unwrap();
777            let snap2 = bridge.snapshot(100).unwrap();
778
779            // Same frame should return same hash
780            assert_eq!(snap1.state_hash, snap2.state_hash);
781        }
782
783        #[test]
784        fn test_record_delta() {
785            let view = MemoryView::new(1024);
786            let mut bridge = StateBridge::direct(view);
787
788            let delta = StateDelta::empty(0);
789            bridge.record_delta(delta);
790
791            assert_eq!(bridge.deltas().len(), 1);
792        }
793
794        #[test]
795        fn test_clear_deltas() {
796            let view = MemoryView::new(1024);
797            let mut bridge = StateBridge::direct(view);
798
799            bridge.record_delta(StateDelta::empty(0));
800            bridge.record_delta(StateDelta::empty(1));
801            bridge.clear_deltas();
802
803            assert!(bridge.deltas().is_empty());
804        }
805
806        #[test]
807        fn test_compute_phash() {
808            let data1 = vec![1, 2, 3, 4, 5];
809            let data2 = vec![1, 2, 3, 4, 5];
810            let data3 = vec![5, 4, 3, 2, 1];
811
812            let hash1 = StateBridge::compute_phash(&data1);
813            let hash2 = StateBridge::compute_phash(&data2);
814            let hash3 = StateBridge::compute_phash(&data3);
815
816            assert_eq!(hash1, hash2);
817            assert_ne!(hash1, hash3);
818        }
819
820        #[test]
821        fn test_visual_compare() {
822            let expected = vec![1, 2, 3, 4, 5];
823            let actual = vec![1, 2, 3, 4, 5];
824
825            let diff = StateBridge::visual_compare(&expected, &actual);
826            assert!(diff.perceptual_similarity > 0.99);
827            assert!(diff.is_identical());
828        }
829
830        #[test]
831        fn test_visual_compare_different() {
832            let expected = vec![1, 2, 3, 4, 5, 6, 7, 8];
833            let actual = vec![8, 7, 6, 5, 4, 3, 2, 1];
834
835            let diff = StateBridge::visual_compare(&expected, &actual);
836            // Should be different
837            assert!(diff.perceptual_similarity < 1.0);
838        }
839    }
840}