1use serde::{Deserialize, Serialize};
3use std::collections::{HashMap, HashSet};
4use std::time::Duration;
5
6use crate::brp_messages::{ComponentTypeId, ComponentValue, EntityData, EntityId};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum ChangeType {
12    EntityAdded,
14    EntityRemoved,
16    EntityModified,
18    ComponentAdded,
20    ComponentRemoved,
22    ComponentModified,
24}
25
26impl ChangeType {
27    #[must_use]
29    pub fn description(&self) -> &'static str {
30        match self {
31            Self::EntityAdded => "Entity added",
32            Self::EntityRemoved => "Entity removed",
33            Self::EntityModified => "Entity modified",
34            Self::ComponentAdded => "Component added",
35            Self::ComponentRemoved => "Component removed",
36            Self::ComponentModified => "Component modified",
37        }
38    }
39
40    #[must_use]
42    pub fn color_code(&self) -> &'static str {
43        match self {
44            Self::EntityAdded | Self::ComponentAdded => "\x1b[32m", Self::EntityRemoved | Self::ComponentRemoved => "\x1b[31m", Self::EntityModified | Self::ComponentModified => "\x1b[33m", }
48    }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Change {
54    pub change_type: ChangeType,
55    pub entity_id: EntityId,
56    pub component_type: Option<ComponentTypeId>,
57    pub old_value: Option<ComponentValue>,
58    pub new_value: Option<ComponentValue>,
59    pub rate_of_change: Option<f64>, pub is_unexpected: bool,         pub timestamp: chrono::DateTime<chrono::Utc>,
62}
63
64impl Change {
65    #[must_use]
67    pub fn new(
68        change_type: ChangeType,
69        entity_id: EntityId,
70        component_type: Option<ComponentTypeId>,
71        old_value: Option<ComponentValue>,
72        new_value: Option<ComponentValue>,
73    ) -> Self {
74        Self {
75            change_type,
76            entity_id,
77            component_type,
78            old_value,
79            new_value,
80            rate_of_change: None,
81            is_unexpected: false,
82            timestamp: chrono::Utc::now(),
83        }
84    }
85
86    pub fn calculate_rate_of_change(&mut self, time_delta: Duration) {
88        if let (Some(old), Some(new)) = (&self.old_value, &self.new_value) {
89            if let (Some(old_num), Some(new_num)) = (old.as_f64(), new.as_f64()) {
90                let delta_seconds = time_delta.as_secs_f64();
91                if delta_seconds > 0.0 {
92                    self.rate_of_change = Some((new_num - old_num) / delta_seconds);
93                }
94            }
95        }
96    }
97
98    pub fn check_unexpected(&mut self, rules: &GameRules) {
100        self.is_unexpected = rules.is_change_unexpected(self);
101    }
102
103    #[must_use]
105    pub fn format_colored(&self) -> String {
106        let color = self.change_type.color_code();
107        let reset = "\x1b[0m";
108
109        let unexpected_marker = if self.is_unexpected { " ⚠️" } else { "" };
110        let rate_info = if let Some(rate) = self.rate_of_change {
111            format!(" (rate: {rate:.3}/s)")
112        } else {
113            String::new()
114        };
115
116        match &self.component_type {
117            Some(component) => {
118                format!(
119                    "{}{} entity {} {}: {}{}{}{}",
120                    color,
121                    self.change_type.description(),
122                    self.entity_id,
123                    component,
124                    self.format_value_change(),
125                    rate_info,
126                    unexpected_marker,
127                    reset
128                )
129            }
130            None => {
131                format!(
132                    "{}{} entity {}{}{}",
133                    color,
134                    self.change_type.description(),
135                    self.entity_id,
136                    unexpected_marker,
137                    reset
138                )
139            }
140        }
141    }
142
143    fn format_value_change(&self) -> String {
144        match (&self.old_value, &self.new_value) {
145            (Some(old), Some(new)) => format!("{} → {}", format_value(old), format_value(new)),
146            (None, Some(new)) => format!("→ {}", format_value(new)),
147            (Some(old), None) => format!("{} → ∅", format_value(old)),
148            (None, None) => String::new(),
149        }
150    }
151}
152
153fn format_value(value: &ComponentValue) -> String {
155    match value {
156        serde_json::Value::Number(n) => {
157            if let Some(f) = n.as_f64() {
158                if f.fract() == 0.0 && f.abs() < 1e15 {
159                    format!("{}", f as i64)
160                } else {
161                    format!("{f:.3}")
162                }
163            } else {
164                n.to_string()
165            }
166        }
167        serde_json::Value::String(s) => format!("\"{s}\""),
168        serde_json::Value::Bool(b) => b.to_string(),
169        serde_json::Value::Array(arr) => {
170            if arr.len() <= 3 {
171                format!(
172                    "[{}]",
173                    arr.iter().map(format_value).collect::<Vec<_>>().join(", ")
174                )
175            } else {
176                format!("[...{}]", arr.len())
177            }
178        }
179        serde_json::Value::Object(obj) => {
180            if obj.len() <= 2 {
181                format!(
182                    "{{{}}}",
183                    obj.iter()
184                        .map(|(k, v)| format!("{}: {}", k, format_value(v)))
185                        .collect::<Vec<_>>()
186                        .join(", ")
187                )
188            } else {
189                format!("{{...{}}}", obj.len())
190            }
191        }
192        serde_json::Value::Null => "null".to_string(),
193    }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ChangeGroup {
199    pub group_type: String,
200    pub changes: Vec<Change>,
201    pub summary: String,
202}
203
204impl ChangeGroup {
205    #[must_use]
207    pub fn new(group_type: String, changes: Vec<Change>) -> Self {
208        let summary = format!("{} changes of type {}", changes.len(), group_type);
209        Self {
210            group_type,
211            changes,
212            summary,
213        }
214    }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct StateSnapshot {
220    pub entities: HashMap<EntityId, EntityData>,
221    pub timestamp: chrono::DateTime<chrono::Utc>,
222    pub generation: u64, }
224
225impl StateSnapshot {
226    #[must_use]
228    pub fn new(entities: Vec<EntityData>, generation: u64) -> Self {
229        let entity_map = entities
230            .into_iter()
231            .map(|entity| (entity.id, entity))
232            .collect();
233
234        Self {
235            entities: entity_map,
236            timestamp: chrono::Utc::now(),
237            generation,
238        }
239    }
240
241    #[must_use]
243    pub fn get_entity(&self, id: EntityId) -> Option<&EntityData> {
244        self.entities.get(&id)
245    }
246
247    #[must_use]
249    pub fn entity_ids(&self) -> HashSet<EntityId> {
250        self.entities.keys().copied().collect()
251    }
252
253    #[must_use]
255    pub fn estimated_compressed_size(&self) -> usize {
256        serde_json::to_string(&self.entities)
258            .map(|s| s.len() / 3) .unwrap_or(0)
260    }
261
262    #[must_use]
264    pub fn compress(&self) -> Vec<u8> {
265        serde_json::to_vec(&self.entities).unwrap_or_default()
268    }
269
270    pub fn decompress(
272        data: &[u8],
273        generation: u64,
274        timestamp: chrono::DateTime<chrono::Utc>,
275    ) -> Option<Self> {
276        let entities: HashMap<EntityId, EntityData> = serde_json::from_slice(data).ok()?;
278        Some(Self {
279            entities,
280            timestamp,
281            generation,
282        })
283    }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct FuzzyCompareConfig {
289    pub epsilon: f64,
290    pub relative_tolerance: f64,
291}
292
293impl Default for FuzzyCompareConfig {
294    fn default() -> Self {
295        Self {
296            epsilon: 1e-6,
297            relative_tolerance: 1e-9,
298        }
299    }
300}
301
302pub trait FuzzyPartialEq {
304    fn fuzzy_eq(&self, other: &Self, config: &FuzzyCompareConfig) -> bool;
305}
306
307impl FuzzyPartialEq for ComponentValue {
308    fn fuzzy_eq(&self, other: &Self, config: &FuzzyCompareConfig) -> bool {
309        match (self, other) {
310            (serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
311                if let (Some(a_f), Some(b_f)) = (a.as_f64(), b.as_f64()) {
312                    if a_f.is_nan() && b_f.is_nan() {
314                        return true; }
316                    if a_f.is_infinite() || b_f.is_infinite() {
317                        return a_f == b_f; }
319
320                    let diff = (a_f - b_f).abs();
321                    let max_val = a_f.abs().max(b_f.abs());
322                    diff <= config.epsilon || diff <= max_val * config.relative_tolerance
323                } else {
324                    a == b
325                }
326            }
327            (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
328                a.len() == b.len()
329                    && a.iter()
330                        .all(|(k, v)| b.get(k).is_some_and(|v2| v.fuzzy_eq(v2, config)))
331            }
332            (serde_json::Value::Array(a), serde_json::Value::Array(b)) => {
333                a.len() == b.len()
334                    && a.iter()
335                        .zip(b.iter())
336                        .all(|(v1, v2)| v1.fuzzy_eq(v2, config))
337            }
338            _ => self == other,
339        }
340    }
341}
342
343#[derive(Debug, Clone, Default)]
345pub struct GameRules {
346    pub max_position_change_per_second: Option<f64>,
347    pub max_velocity_change_per_second: Option<f64>,
348    pub immutable_components: HashSet<ComponentTypeId>,
349    pub expected_value_ranges: HashMap<String, (f64, f64)>, }
351
352impl GameRules {
353    #[must_use]
355    pub fn is_change_unexpected(&self, change: &Change) -> bool {
356        if let Some(component_type) = &change.component_type {
358            if self.immutable_components.contains(component_type) {
359                return true;
360            }
361        }
362
363        if let Some(rate) = change.rate_of_change {
365            if let Some(component_type) = &change.component_type {
366                match component_type.as_str() {
367                    "Transform" => {
368                        if let Some(max_pos_rate) = self.max_position_change_per_second {
369                            if rate.abs() > max_pos_rate {
370                                return true;
371                            }
372                        }
373                    }
374                    "Velocity" => {
375                        if let Some(max_vel_rate) = self.max_velocity_change_per_second {
376                            if rate.abs() > max_vel_rate {
377                                return true;
378                            }
379                        }
380                    }
381                    _ => {}
382                }
383            }
384        }
385
386        if let (Some(component_type), Some(new_value)) = (&change.component_type, &change.new_value)
388        {
389            if let Some(num_value) = new_value.as_f64() {
390                let field_key = format!("{component_type}.value");
391                if let Some((min, max)) = self.expected_value_ranges.get(&field_key) {
392                    if num_value < *min || num_value > *max {
393                        return true;
394                    }
395                }
396            }
397        }
398
399        false
400    }
401}
402
403pub struct StateDiff {
405    fuzzy_config: FuzzyCompareConfig,
406    game_rules: GameRules,
407    generation_counter: u64,
408}
409
410impl StateDiff {
411    #[must_use]
413    pub fn new() -> Self {
414        Self {
415            fuzzy_config: FuzzyCompareConfig::default(),
416            game_rules: GameRules::default(),
417            generation_counter: 0,
418        }
419    }
420
421    #[must_use]
423    pub fn with_config(fuzzy_config: FuzzyCompareConfig, game_rules: GameRules) -> Self {
424        Self {
425            fuzzy_config,
426            game_rules,
427            generation_counter: 0,
428        }
429    }
430
431    pub fn create_snapshot(&mut self, entities: Vec<EntityData>) -> StateSnapshot {
433        self.generation_counter = self.generation_counter.wrapping_add(1);
434        StateSnapshot::new(entities, self.generation_counter)
435    }
436
437    pub fn diff_snapshots(&self, before: &StateSnapshot, after: &StateSnapshot) -> StateDiffResult {
439        let mut changes = Vec::new();
440        let time_delta = after.timestamp.signed_duration_since(before.timestamp);
441        let time_delta_std = Duration::from_millis(time_delta.num_milliseconds().max(0) as u64);
442
443        let before_ids = before.entity_ids();
444        let after_ids = after.entity_ids();
445
446        for entity_id in after_ids.difference(&before_ids) {
448            changes.push(Change::new(
449                ChangeType::EntityAdded,
450                *entity_id,
451                None,
452                None,
453                None,
454            ));
455        }
456
457        for entity_id in before_ids.difference(&after_ids) {
459            changes.push(Change::new(
460                ChangeType::EntityRemoved,
461                *entity_id,
462                None,
463                None,
464                None,
465            ));
466        }
467
468        for entity_id in before_ids.intersection(&after_ids) {
470            if let (Some(before_entity), Some(after_entity)) =
471                (before.get_entity(*entity_id), after.get_entity(*entity_id))
472            {
473                let entity_changes = self.diff_entity(before_entity, after_entity, time_delta_std);
474                changes.extend(entity_changes);
475            }
476        }
477
478        for change in &mut changes {
480            change.check_unexpected(&self.game_rules);
481        }
482
483        StateDiffResult::new(changes, before.clone(), after.clone())
484    }
485
486    fn diff_entity(
488        &self,
489        before: &EntityData,
490        after: &EntityData,
491        time_delta: Duration,
492    ) -> Vec<Change> {
493        let mut changes = Vec::new();
494
495        let before_components: HashSet<&ComponentTypeId> = before.components.keys().collect();
496        let after_components: HashSet<&ComponentTypeId> = after.components.keys().collect();
497
498        for component_type in after_components.difference(&before_components) {
500            let new_value = after.components.get(*component_type).unwrap();
501            changes.push(Change::new(
502                ChangeType::ComponentAdded,
503                after.id,
504                Some(component_type.to_string()),
505                None,
506                Some(new_value.clone()),
507            ));
508        }
509
510        for component_type in before_components.difference(&after_components) {
512            let old_value = before.components.get(*component_type).unwrap();
513            changes.push(Change::new(
514                ChangeType::ComponentRemoved,
515                before.id,
516                Some(component_type.to_string()),
517                Some(old_value.clone()),
518                None,
519            ));
520        }
521
522        for component_type in before_components.intersection(&after_components) {
524            let before_value = before.components.get(*component_type).unwrap();
525            let after_value = after.components.get(*component_type).unwrap();
526
527            if !before_value.fuzzy_eq(after_value, &self.fuzzy_config) {
528                let mut change = Change::new(
529                    ChangeType::ComponentModified,
530                    before.id,
531                    Some(component_type.to_string()),
532                    Some(before_value.clone()),
533                    Some(after_value.clone()),
534                );
535                change.calculate_rate_of_change(time_delta);
536                changes.push(change);
537            }
538        }
539
540        changes
541    }
542
543    #[must_use]
545    pub fn group_changes(&self, changes: &[Change]) -> Vec<ChangeGroup> {
546        let mut groups: HashMap<String, Vec<Change>> = HashMap::new();
547
548        for change in changes {
549            let group_key = match &change.component_type {
550                Some(component) => format!("{component} changes"),
551                None => "Entity changes".to_string(),
552            };
553            groups.entry(group_key).or_default().push(change.clone());
554        }
555
556        groups
557            .into_iter()
558            .map(|(group_type, changes)| ChangeGroup::new(group_type, changes))
559            .collect()
560    }
561
562    pub fn set_fuzzy_config(&mut self, config: FuzzyCompareConfig) {
564        self.fuzzy_config = config;
565    }
566
567    pub fn set_game_rules(&mut self, rules: GameRules) {
569        self.game_rules = rules;
570    }
571
572    #[must_use]
574    pub fn generation_counter(&self) -> u64 {
575        self.generation_counter
576    }
577
578    #[must_use]
580    pub fn fuzzy_config(&self) -> &FuzzyCompareConfig {
581        &self.fuzzy_config
582    }
583}
584
585impl Default for StateDiff {
586    fn default() -> Self {
587        Self::new()
588    }
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct StateDiffResult {
594    pub changes: Vec<Change>,
595    pub before_snapshot: StateSnapshot,
596    pub after_snapshot: StateSnapshot,
597    pub summary: DiffSummary,
598}
599
600impl StateDiffResult {
601    #[must_use]
603    pub fn new(changes: Vec<Change>, before: StateSnapshot, after: StateSnapshot) -> Self {
604        let summary = DiffSummary::from_changes(&changes);
605        Self {
606            changes,
607            before_snapshot: before,
608            after_snapshot: after,
609            summary,
610        }
611    }
612
613    #[must_use]
615    pub fn filter_by_type(&self, change_type: ChangeType) -> Vec<&Change> {
616        self.changes
617            .iter()
618            .filter(|change| change.change_type == change_type)
619            .collect()
620    }
621
622    #[must_use]
624    pub fn unexpected_changes(&self) -> Vec<&Change> {
625        self.changes
626            .iter()
627            .filter(|change| change.is_unexpected)
628            .collect()
629    }
630
631    #[must_use]
633    pub fn format_colored(&self) -> String {
634        let mut output = Vec::new();
635
636        output.push(format!(
637            "State Diff ({} → {})",
638            self.before_snapshot.timestamp.format("%H:%M:%S%.3f"),
639            self.after_snapshot.timestamp.format("%H:%M:%S%.3f")
640        ));
641        output.push(format!("Summary: {}", self.summary.format()));
642        output.push(String::new());
643
644        if self.changes.is_empty() {
645            output.push("No changes detected.".to_string());
646        } else {
647            for change in &self.changes {
648                output.push(change.format_colored());
649            }
650        }
651
652        output.join("\n")
653    }
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct DiffSummary {
659    pub entities_added: usize,
660    pub entities_removed: usize,
661    pub entities_modified: usize,
662    pub components_added: usize,
663    pub components_removed: usize,
664    pub components_modified: usize,
665    pub unexpected_changes: usize,
666    pub total_changes: usize,
667}
668
669impl DiffSummary {
670    #[must_use]
672    pub fn from_changes(changes: &[Change]) -> Self {
673        let mut summary = Self {
674            entities_added: 0,
675            entities_removed: 0,
676            entities_modified: 0,
677            components_added: 0,
678            components_removed: 0,
679            components_modified: 0,
680            unexpected_changes: 0,
681            total_changes: changes.len(),
682        };
683
684        for change in changes {
685            match change.change_type {
686                ChangeType::EntityAdded => summary.entities_added += 1,
687                ChangeType::EntityRemoved => summary.entities_removed += 1,
688                ChangeType::EntityModified => summary.entities_modified += 1,
689                ChangeType::ComponentAdded => summary.components_added += 1,
690                ChangeType::ComponentRemoved => summary.components_removed += 1,
691                ChangeType::ComponentModified => summary.components_modified += 1,
692            }
693
694            if change.is_unexpected {
695                summary.unexpected_changes += 1;
696            }
697        }
698
699        summary
700    }
701
702    #[must_use]
704    pub fn format(&self) -> String {
705        let mut parts = Vec::new();
706
707        if self.entities_added > 0 {
708            parts.push(format!("+{} entities", self.entities_added));
709        }
710        if self.entities_removed > 0 {
711            parts.push(format!("-{} entities", self.entities_removed));
712        }
713        if self.entities_modified > 0 {
714            parts.push(format!("~{} entities", self.entities_modified));
715        }
716        if self.components_added > 0 {
717            parts.push(format!("+{} components", self.components_added));
718        }
719        if self.components_removed > 0 {
720            parts.push(format!("-{} components", self.components_removed));
721        }
722        if self.components_modified > 0 {
723            parts.push(format!("~{} components", self.components_modified));
724        }
725
726        let base_summary = if parts.is_empty() {
727            "No changes".to_string()
728        } else {
729            parts.join(", ")
730        };
731
732        if self.unexpected_changes > 0 {
733            format!("{} ({} unexpected)", base_summary, self.unexpected_changes)
734        } else {
735            base_summary
736        }
737    }
738}
739
740#[derive(Debug, Clone)]
742pub struct DiffTimeWindow {
743    pub start: chrono::DateTime<chrono::Utc>,
744    pub end: chrono::DateTime<chrono::Utc>,
745    pub snapshots: Vec<StateSnapshot>,
746    pub max_snapshots: usize, }
748
749impl DiffTimeWindow {
750    #[must_use]
752    pub fn new(start: chrono::DateTime<chrono::Utc>, end: chrono::DateTime<chrono::Utc>) -> Self {
753        Self {
754            start,
755            end,
756            snapshots: Vec::new(),
757            max_snapshots: 100, }
759    }
760
761    #[must_use]
763    pub fn with_capacity(
764        start: chrono::DateTime<chrono::Utc>,
765        end: chrono::DateTime<chrono::Utc>,
766        max_snapshots: usize,
767    ) -> Self {
768        Self {
769            start,
770            end,
771            snapshots: Vec::new(),
772            max_snapshots,
773        }
774    }
775
776    pub fn add_snapshot(&mut self, snapshot: StateSnapshot) -> bool {
778        if snapshot.timestamp >= self.start && snapshot.timestamp <= self.end {
779            self.snapshots.push(snapshot);
780            self.snapshots.sort_by_key(|s| s.timestamp);
781
782            if self.snapshots.len() > self.max_snapshots {
784                let excess = self.snapshots.len() - self.max_snapshots;
785                self.snapshots.drain(0..excess);
786            }
787
788            true
789        } else {
790            false
791        }
792    }
793
794    pub fn get_merged_diff(&self, diff_engine: &StateDiff) -> Option<StateDiffResult> {
796        if self.snapshots.len() < 2 {
797            return None;
798        }
799
800        let first = self.snapshots.first()?;
801        let last = self.snapshots.last()?;
802
803        Some(diff_engine.diff_snapshots(first, last))
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use serde_json::json;
811
812    fn create_test_entity(id: u64, components: Vec<(&str, serde_json::Value)>) -> EntityData {
813        EntityData {
814            id,
815            components: components
816                .into_iter()
817                .map(|(k, v)| (k.to_string(), v))
818                .collect(),
819        }
820    }
821
822    #[test]
823    fn test_fuzzy_float_comparison() {
824        let config = FuzzyCompareConfig::default();
825
826        let val1 = json!(1.0000001);
827        let val2 = json!(1.0000002);
828        let val3 = json!(1.1);
829
830        assert!(val1.fuzzy_eq(&val2, &config));
831        assert!(!val1.fuzzy_eq(&val3, &config));
832    }
833
834    #[test]
835    fn test_entity_addition() {
836        let mut diff_engine = StateDiff::new();
837
838        let before = diff_engine.create_snapshot(vec![]);
839        let after = diff_engine.create_snapshot(vec![create_test_entity(
840            1,
841            vec![("Transform", json!({"x": 0.0, "y": 0.0}))],
842        )]);
843
844        let result = diff_engine.diff_snapshots(&before, &after);
845        assert_eq!(result.changes.len(), 1);
846        assert_eq!(result.changes[0].change_type, ChangeType::EntityAdded);
847        assert_eq!(result.changes[0].entity_id, 1);
848    }
849
850    #[test]
851    fn test_entity_removal() {
852        let mut diff_engine = StateDiff::new();
853
854        let before = diff_engine.create_snapshot(vec![create_test_entity(
855            1,
856            vec![("Transform", json!({"x": 0.0, "y": 0.0}))],
857        )]);
858        let after = diff_engine.create_snapshot(vec![]);
859
860        let result = diff_engine.diff_snapshots(&before, &after);
861        assert_eq!(result.changes.len(), 1);
862        assert_eq!(result.changes[0].change_type, ChangeType::EntityRemoved);
863        assert_eq!(result.changes[0].entity_id, 1);
864    }
865
866    #[test]
867    fn test_component_modification() {
868        let mut diff_engine = StateDiff::new();
869
870        let before = diff_engine.create_snapshot(vec![create_test_entity(
871            1,
872            vec![("Transform", json!({"x": 0.0, "y": 0.0}))],
873        )]);
874        let after = diff_engine.create_snapshot(vec![create_test_entity(
875            1,
876            vec![("Transform", json!({"x": 1.0, "y": 0.0}))],
877        )]);
878
879        let result = diff_engine.diff_snapshots(&before, &after);
880        assert_eq!(result.changes.len(), 1);
881        assert_eq!(result.changes[0].change_type, ChangeType::ComponentModified);
882        assert_eq!(result.changes[0].entity_id, 1);
883        assert_eq!(
884            result.changes[0].component_type,
885            Some("Transform".to_string())
886        );
887    }
888
889    #[test]
890    fn test_component_addition_removal() {
891        let mut diff_engine = StateDiff::new();
892
893        let before = diff_engine.create_snapshot(vec![create_test_entity(
894            1,
895            vec![("Transform", json!({"x": 0.0, "y": 0.0}))],
896        )]);
897        let after = diff_engine.create_snapshot(vec![create_test_entity(
898            1,
899            vec![
900                ("Transform", json!({"x": 0.0, "y": 0.0})),
901                ("Velocity", json!({"vx": 1.0, "vy": 0.0})),
902            ],
903        )]);
904
905        let result = diff_engine.diff_snapshots(&before, &after);
906        assert_eq!(result.changes.len(), 1);
907        assert_eq!(result.changes[0].change_type, ChangeType::ComponentAdded);
908        assert_eq!(
909            result.changes[0].component_type,
910            Some("Velocity".to_string())
911        );
912    }
913
914    #[test]
915    fn test_game_rules_unexpected_changes() {
916        let mut diff_engine = StateDiff::new();
917        let mut rules = GameRules::default();
918        rules.max_position_change_per_second = Some(10.0);
919        diff_engine.set_game_rules(rules);
920
921        let before = diff_engine.create_snapshot(vec![create_test_entity(
922            1,
923            vec![("Transform", json!({"x": 0.0}))],
924        )]);
925
926        std::thread::sleep(std::time::Duration::from_millis(100));
928
929        let after = diff_engine.create_snapshot(vec![
930            create_test_entity(1, vec![("Transform", json!({"x": 100.0}))]), ]);
932
933        let result = diff_engine.diff_snapshots(&before, &after);
934        assert_eq!(result.changes.len(), 1);
935        }
938
939    #[test]
940    fn test_change_grouping() {
941        let mut diff_engine = StateDiff::new();
942
943        let changes = vec![
944            Change::new(
945                ChangeType::ComponentModified,
946                1,
947                Some("Transform".to_string()),
948                None,
949                None,
950            ),
951            Change::new(
952                ChangeType::ComponentModified,
953                2,
954                Some("Transform".to_string()),
955                None,
956                None,
957            ),
958            Change::new(
959                ChangeType::ComponentModified,
960                1,
961                Some("Velocity".to_string()),
962                None,
963                None,
964            ),
965        ];
966
967        let groups = diff_engine.group_changes(&changes);
968        assert_eq!(groups.len(), 2); let transform_group = groups
971            .iter()
972            .find(|g| g.group_type == "Transform changes")
973            .unwrap();
974        assert_eq!(transform_group.changes.len(), 2);
975    }
976
977    #[test]
978    fn test_diff_summary() {
979        let changes = vec![
980            Change::new(ChangeType::EntityAdded, 1, None, None, None),
981            Change::new(ChangeType::EntityRemoved, 2, None, None, None),
982            Change::new(
983                ChangeType::ComponentModified,
984                3,
985                Some("Transform".to_string()),
986                None,
987                None,
988            ),
989        ];
990
991        let summary = DiffSummary::from_changes(&changes);
992        assert_eq!(summary.entities_added, 1);
993        assert_eq!(summary.entities_removed, 1);
994        assert_eq!(summary.components_modified, 1);
995        assert_eq!(summary.total_changes, 3);
996
997        let formatted = summary.format();
998        assert!(formatted.contains("+1 entities"));
999        assert!(formatted.contains("-1 entities"));
1000        assert!(formatted.contains("~1 components"));
1001    }
1002
1003    #[test]
1004    fn test_snapshot_creation() {
1005        let mut diff_engine = StateDiff::new();
1006
1007        let entities = vec![
1008            create_test_entity(1, vec![("Transform", json!({"x": 0.0}))]),
1009            create_test_entity(2, vec![("Health", json!(100))]),
1010        ];
1011
1012        let snapshot = diff_engine.create_snapshot(entities);
1013        assert_eq!(snapshot.entities.len(), 2);
1014        assert_eq!(snapshot.generation, 1);
1015        assert!(snapshot.get_entity(1).is_some());
1016        assert!(snapshot.get_entity(3).is_none());
1017    }
1018
1019    #[test]
1020    fn test_time_window() {
1021        let mut diff_engine = StateDiff::new();
1022        let now = chrono::Utc::now();
1023        let mut window = DiffTimeWindow::new(now, now + chrono::Duration::seconds(10));
1024
1025        let snapshot1 = diff_engine.create_snapshot(vec![create_test_entity(
1026            1,
1027            vec![("Transform", json!({"x": 0.0}))],
1028        )]);
1029        let snapshot2 = diff_engine.create_snapshot(vec![create_test_entity(
1030            1,
1031            vec![("Transform", json!({"x": 1.0}))],
1032        )]);
1033
1034        assert!(window.add_snapshot(snapshot1));
1035        assert!(window.add_snapshot(snapshot2));
1036
1037        let merged_diff = window.get_merged_diff(&diff_engine);
1038        assert!(merged_diff.is_some());
1039
1040        let diff_result = merged_diff.unwrap();
1041        assert_eq!(diff_result.changes.len(), 1);
1042        assert_eq!(
1043            diff_result.changes[0].change_type,
1044            ChangeType::ComponentModified
1045        );
1046    }
1047}