Skip to main content

apif_assert/
comparator.rs

1use super::engine::AssertionResult;
2use apif_ast::ast::InlineOptions;
3use serde_json::Value;
4
5pub struct JsonComparator;
6
7impl JsonComparator {
8    pub fn compare(
9        actual: &Value,
10        expected: &Value,
11        options: &InlineOptions,
12    ) -> Vec<AssertionResult> {
13        let mut results = Vec::new();
14
15        if options.redact.is_empty() {
16            Self::compare_recursive(actual, expected, "$", options, &mut results);
17        } else {
18            let mut actual_redacted = actual.clone();
19            Self::redact_value(&mut actual_redacted, &options.redact);
20            Self::compare_recursive(&actual_redacted, expected, "$", options, &mut results);
21        }
22
23        results
24    }
25
26    fn redact_value(value: &mut Value, fields: &[String]) {
27        match value {
28            Value::Object(map) => {
29                for field in fields {
30                    map.remove(field);
31                }
32                for (_, v) in map.iter_mut() {
33                    Self::redact_value(v, fields);
34                }
35            }
36            Value::Array(arr) => {
37                for v in arr.iter_mut() {
38                    Self::redact_value(v, fields);
39                }
40            }
41            _ => {}
42        }
43    }
44
45    fn compare_recursive(
46        actual: &Value,
47        expected: &Value,
48        path: &str,
49        options: &InlineOptions,
50        results: &mut Vec<AssertionResult>,
51    ) {
52        if let Value::String(s) = expected
53            && s == "*"
54        {
55            return; // Matches anything
56        }
57
58        // Numbers can be float/int, so strictly checking discriminants might be too harsh if serde parses differently.
59        // But generally types should match.
60        // Exception: expected "*" string matches any actual type (handled above).
61
62        match (actual, expected) {
63            (Value::Object(act_map), Value::Object(exp_map)) => {
64                // For objects, iterate over EXPECTED keys
65                for (k, exp_val) in exp_map {
66                    let new_path = format!("{}.{}", path, k);
67
68                    if let Some(act_val) = act_map.get(k) {
69                        Self::compare_recursive(act_val, exp_val, &new_path, options, results);
70                    } else {
71                        // Proto JSON may omit fields with default values.
72                        // If expected value is a default, treat missing key as acceptable.
73                        if !is_protojson_default_value(exp_val) {
74                            results.push(AssertionResult::fail(format!(
75                                "Key '{}' missing in actual response",
76                                new_path
77                            )));
78                        }
79                    }
80                }
81
82                if !options.partial {
83                    for k in act_map.keys() {
84                        if !exp_map.contains_key(k) {
85                            results.push(AssertionResult::fail(format!(
86                                "Unexpected key '{}.{}' in actual response",
87                                path, k
88                            )));
89                        }
90                    }
91                }
92            }
93            (Value::Array(act_arr), Value::Array(exp_arr)) => {
94                if !options.partial && act_arr.len() != exp_arr.len() {
95                    results.push(AssertionResult::fail_with_diff(
96                        format!(
97                            "Array length mismatch at '{}': expected {}, got {}",
98                            path,
99                            exp_arr.len(),
100                            act_arr.len()
101                        ),
102                        format!("length: {}", exp_arr.len()),
103                        format!("length: {}", act_arr.len()),
104                    ));
105                }
106
107                if options.unordered_arrays {
108                    // OPTIMIZED: Hash-based O(n) comparison instead of O(n²)
109                    // Strategy: Hash each item and compare hash sets
110                    // For items with same hash, do deep comparison
111
112                    let mut matched_actual_indices = std::collections::HashSet::new();
113                    let mut hash_to_indices: std::collections::HashMap<u64, Vec<usize>> =
114                        std::collections::HashMap::new();
115
116                    for (i, act_item) in act_arr.iter().enumerate() {
117                        let hash = Self::hash_value(act_item);
118                        hash_to_indices.entry(hash).or_default().push(i);
119                    }
120
121                    for exp_item in exp_arr {
122                        let exp_hash = Self::hash_value(exp_item);
123                        let mut found = false;
124
125                        // Fast path: candidates with an identical hash
126                        if let Some(indices) = hash_to_indices.get(&exp_hash) {
127                            for &idx in indices.iter() {
128                                if matched_actual_indices.contains(&idx) {
129                                    continue;
130                                }
131
132                                let mut temp_results = Vec::new();
133                                Self::compare_recursive(
134                                    &act_arr[idx],
135                                    exp_item,
136                                    &format!("{}[{}]", path, idx),
137                                    options,
138                                    &mut temp_results,
139                                );
140
141                                if temp_results.is_empty() {
142                                    matched_actual_indices.insert(idx);
143                                    found = true;
144                                    break;
145                                }
146                            }
147                        }
148
149                        // Slow path: the hash prefilter misses fuzzy matches
150                        // (60 vs 60.0, wildcards, tolerance, partial objects),
151                        // so fall back to deep comparison against every
152                        // unmatched actual item.
153                        if !found {
154                            for (idx, act_item) in act_arr.iter().enumerate() {
155                                if matched_actual_indices.contains(&idx) {
156                                    continue;
157                                }
158
159                                let mut temp_results = Vec::new();
160                                Self::compare_recursive(
161                                    act_item,
162                                    exp_item,
163                                    &format!("{}[{}]", path, idx),
164                                    options,
165                                    &mut temp_results,
166                                );
167
168                                if temp_results.is_empty() {
169                                    matched_actual_indices.insert(idx);
170                                    found = true;
171                                    break;
172                                }
173                            }
174                        }
175
176                        // Every expected item must be present, even in partial
177                        // mode (partial only allows extra actual items).
178                        if !found {
179                            results.push(AssertionResult::fail(format!(
180                                "Missing expected item in unordered array at '{}': {:?}",
181                                path, exp_item
182                            )));
183                        }
184                    }
185
186                    if !options.partial && matched_actual_indices.len() < act_arr.len() {
187                        results.push(AssertionResult::fail(format!(
188                            "Unordered array at '{}' has {} extra items",
189                            path,
190                            act_arr.len() - matched_actual_indices.len()
191                        )));
192                    }
193
194                    return;
195                }
196
197                // If partial is true, we usually still expect the items we defined to match the *first* N items
198                // OR we strictly match what we have.
199                // Let's implement strict index matching for the common case.
200
201                let len = std::cmp::min(act_arr.len(), exp_arr.len());
202                for i in 0..len {
203                    let new_path = format!("{}[{}]", path, i);
204                    Self::compare_recursive(&act_arr[i], &exp_arr[i], &new_path, options, results);
205                }
206
207                // If expected is longer than actual, that's always a fail (missing items)
208                if exp_arr.len() > act_arr.len() {
209                    for i in act_arr.len()..exp_arr.len() {
210                        results.push(AssertionResult::fail(format!(
211                            "Missing array item at '{}[{}]'",
212                            path, i
213                        )));
214                    }
215                }
216            }
217            (Value::String(a), Value::String(e)) => {
218                if a != e {
219                    results.push(AssertionResult::fail_with_diff(
220                        format!(
221                            "Value mismatch at '{}': expected \"{}\", got \"{}\"",
222                            path, e, a
223                        ),
224                        e,
225                        a,
226                    ));
227                }
228            }
229            (Value::Number(a), Value::Number(e)) => {
230                if let Some(tol) = options.tolerance
231                    && let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64())
232                {
233                    if (af - ef).abs() > tol {
234                        results.push(AssertionResult::fail_with_diff(
235                            format!(
236                                "Value mismatch at '{}': expected {} (tolerance {}), got {}",
237                                path, ef, tol, af
238                            ),
239                            format!("{} (±{})", ef, tol),
240                            format!("{}", af),
241                        ));
242                    }
243                    return;
244                }
245
246                // Integer vs integer: exact comparison (going through f64 would
247                // lose precision above 2^53 and make e.g. i64::MAX == i64::MAX - 1).
248                let a_is_int = a.is_i64() || a.is_u64();
249                let e_is_int = e.is_i64() || e.is_u64();
250                if a_is_int && e_is_int {
251                    let equal = if let (Some(ai), Some(ei)) = (a.as_i64(), e.as_i64()) {
252                        ai == ei
253                    } else if let (Some(au), Some(eu)) = (a.as_u64(), e.as_u64()) {
254                        au == eu
255                    } else {
256                        // One is negative, the other exceeds i64::MAX
257                        false
258                    };
259                    if equal {
260                        return;
261                    }
262                } else if let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64()) {
263                    // At least one side is a float. Treat numerically-equal
264                    // values as equal even if JSON representation differs
265                    // (e.g. 60 vs 60.0), and absorb f64 rounding noise with a
266                    // relative epsilon that scales with magnitude.
267                    let scale = af.abs().max(ef.abs());
268                    if af == ef || (af - ef).abs() <= 1e-9 * scale {
269                        return;
270                    }
271                }
272
273                results.push(AssertionResult::fail_with_diff(
274                    format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
275                    format!("{}", e),
276                    format!("{}", a),
277                ));
278            }
279            (Value::Bool(a), Value::Bool(e)) => {
280                if a != e {
281                    results.push(AssertionResult::fail_with_diff(
282                        format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
283                        format!("{}", e),
284                        format!("{}", a),
285                    ));
286                }
287            }
288            (Value::Null, Value::Null) => {}
289            _ => {
290                results.push(AssertionResult::fail_with_diff(
291                    format!(
292                        "Type mismatch at '{}': expected {:?}, got {:?}",
293                        path, expected, actual
294                    ),
295                    format!("{:?}", expected),
296                    format!("{:?}", actual),
297                ));
298            }
299        }
300    }
301
302    /// Hash a JSON value for fast comparison
303    /// Uses a simple hash combining approach for efficiency
304    fn hash_value(value: &Value) -> u64 {
305        use std::collections::hash_map::DefaultHasher;
306        use std::hash::{Hash, Hasher};
307
308        let mut hasher = DefaultHasher::new();
309
310        match value {
311            Value::Null => 0u8.hash(&mut hasher),
312            Value::Bool(b) => (1u8, b).hash(&mut hasher),
313            Value::Number(n) => {
314                (2u8, n.as_i64(), n.as_u64(), n.as_f64().map(|f| f.to_bits())).hash(&mut hasher)
315            }
316            Value::String(s) => (3u8, s).hash(&mut hasher),
317            Value::Array(arr) => {
318                (4u8, arr.len()).hash(&mut hasher);
319                for item in arr {
320                    Self::hash_value(item).hash(&mut hasher);
321                }
322            }
323            Value::Object(obj) => {
324                (5u8, obj.len()).hash(&mut hasher);
325                // Sort keys for consistent hashing
326                let mut keys: Vec<_> = obj.keys().collect();
327                keys.sort();
328                for key in keys {
329                    key.hash(&mut hasher);
330                    Self::hash_value(&obj[key]).hash(&mut hasher);
331                }
332            }
333        }
334
335        hasher.finish()
336    }
337}
338
339fn is_protojson_default_value(value: &Value) -> bool {
340    match value {
341        Value::Null => true,
342        Value::Bool(b) => !*b,
343        Value::Number(n) => {
344            if let Some(i) = n.as_i64() {
345                i == 0
346            } else if let Some(u) = n.as_u64() {
347                u == 0
348            } else if let Some(f) = n.as_f64() {
349                f == 0.0
350            } else {
351                false
352            }
353        }
354        Value::String(s) => s.is_empty(),
355        Value::Array(arr) => arr.is_empty(),
356        Value::Object(map) => map.is_empty(),
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use serde_json::json;
364
365    #[test]
366    fn test_compare_exact_match() {
367        let actual = json!({"foo": "bar", "num": 1});
368        let expected = json!({"foo": "bar", "num": 1});
369        let options = InlineOptions::default();
370
371        let results = JsonComparator::compare(&actual, &expected, &options);
372        assert!(results.is_empty());
373    }
374
375    #[test]
376    fn test_compare_numeric_representation_match() {
377        let actual = json!({"result": 60.0});
378        let expected = json!({"result": 60});
379        let options = InlineOptions::default();
380
381        let results = JsonComparator::compare(&actual, &expected, &options);
382        assert!(results.is_empty());
383    }
384
385    #[test]
386    fn test_compare_mismatch() {
387        let actual = json!({"foo": "bar"});
388        let expected = json!({"foo": "baz"});
389        let options = InlineOptions::default();
390
391        let results = JsonComparator::compare(&actual, &expected, &options);
392        assert_eq!(results.len(), 1);
393        if let AssertionResult::Fail { message: msg, .. } = &results[0] {
394            assert!(msg.contains("Value mismatch"));
395        } else {
396            panic!("Expected Fail");
397        }
398    }
399
400    #[test]
401    fn test_compare_partial_object() {
402        let actual = json!({"foo": "bar", "extra": "field"});
403        let expected = json!({"foo": "bar"});
404
405        // Without partial, this should fail (unexpected key)
406        let options = InlineOptions::default();
407        let results = JsonComparator::compare(&actual, &expected, &options);
408        assert_eq!(results.len(), 1);
409
410        // With partial, this should pass
411        let options = InlineOptions {
412            partial: true,
413            ..Default::default()
414        };
415        let results = JsonComparator::compare(&actual, &expected, &options);
416        assert!(results.is_empty());
417    }
418
419    #[test]
420    fn test_wildcard() {
421        let actual = json!({"id": 12345, "name": "test"});
422        let expected = json!({"id": "*", "name": "test"});
423        let options = InlineOptions::default();
424
425        let results = JsonComparator::compare(&actual, &expected, &options);
426        assert!(results.is_empty());
427    }
428
429    #[test]
430    fn test_redact() {
431        let actual = json!({"id": 12345, "secret": "hidden", "name": "test"});
432        // If we redact "secret", it's removed from actual.
433        // If expected doesn't have it, strict match should pass.
434        let expected = json!({"id": 12345, "name": "test"});
435
436        let options = InlineOptions {
437            redact: vec!["secret".to_string()],
438            ..Default::default()
439        };
440
441        let results = JsonComparator::compare(&actual, &expected, &options);
442        assert!(results.is_empty());
443    }
444
445    #[test]
446    fn test_tolerance() {
447        let actual = json!({"val": 10.005});
448        let expected = json!({"val": 10.0});
449
450        let mut options = InlineOptions {
451            tolerance: Some(0.01),
452            ..Default::default()
453        };
454
455        let results = JsonComparator::compare(&actual, &expected, &options);
456        assert!(results.is_empty());
457
458        options.tolerance = Some(0.001);
459        let results = JsonComparator::compare(&actual, &expected, &options);
460        assert_eq!(results.len(), 1);
461    }
462
463    #[test]
464    fn test_compare_empty_objects() {
465        let actual = json!({});
466        let expected = json!({});
467        let options = InlineOptions::default();
468
469        let results = JsonComparator::compare(&actual, &expected, &options);
470        assert!(results.is_empty());
471    }
472
473    #[test]
474    fn test_compare_empty_arrays() {
475        let actual = json!([]);
476        let expected = json!([]);
477        let options = InlineOptions::default();
478
479        let results = JsonComparator::compare(&actual, &expected, &options);
480        assert!(results.is_empty());
481    }
482
483    #[test]
484    fn test_compare_null_values() {
485        let actual = json!({"val": null});
486        let expected = json!({"val": null});
487        let options = InlineOptions::default();
488
489        let results = JsonComparator::compare(&actual, &expected, &options);
490        assert!(results.is_empty());
491    }
492
493    #[test]
494    fn test_compare_null_mismatch() {
495        let actual = json!({"val": "not null"});
496        let expected = json!({"val": null});
497        let options = InlineOptions::default();
498
499        let results = JsonComparator::compare(&actual, &expected, &options);
500        assert_eq!(results.len(), 1);
501    }
502
503    #[test]
504    fn test_compare_boolean_values() {
505        let actual = json!({"active": true, "deleted": false});
506        let expected = json!({"active": true, "deleted": false});
507        let options = InlineOptions::default();
508
509        let results = JsonComparator::compare(&actual, &expected, &options);
510        assert!(results.is_empty());
511    }
512
513    #[test]
514    fn test_compare_boolean_mismatch() {
515        let actual = json!({"active": true});
516        let expected = json!({"active": false});
517        let options = InlineOptions::default();
518
519        let results = JsonComparator::compare(&actual, &expected, &options);
520        assert_eq!(results.len(), 1);
521    }
522
523    #[test]
524    fn test_compare_nested_objects() {
525        let actual = json!({"user": {"name": "test", "age": 25}});
526        let expected = json!({"user": {"name": "test", "age": 25}});
527        let options = InlineOptions::default();
528
529        let results = JsonComparator::compare(&actual, &expected, &options);
530        assert!(results.is_empty());
531    }
532
533    #[test]
534    fn test_compare_nested_mismatch() {
535        let actual = json!({"user": {"name": "test"}});
536        let expected = json!({"user": {"name": "other"}});
537        let options = InlineOptions::default();
538
539        let results = JsonComparator::compare(&actual, &expected, &options);
540        assert_eq!(results.len(), 1);
541    }
542
543    #[test]
544    fn test_compare_arrays_different_lengths() {
545        let actual = json!([1, 2, 3]);
546        let expected = json!([1, 2]);
547        let options = InlineOptions::default();
548
549        let results = JsonComparator::compare(&actual, &expected, &options);
550        assert!(!results.is_empty());
551    }
552
553    #[test]
554    fn test_compare_arrays_with_objects() {
555        let actual = json!([{"id": 1}, {"id": 2}]);
556        let expected = json!([{"id": 1}, {"id": 2}]);
557        let options = InlineOptions::default();
558
559        let results = JsonComparator::compare(&actual, &expected, &options);
560        assert!(results.is_empty());
561    }
562
563    #[test]
564    fn test_compare_partial_nested_object() {
565        let actual = json!({"user": {"name": "test", "age": 25, "extra": "field"}});
566        let expected = json!({"user": {"name": "test"}});
567        let options = InlineOptions {
568            partial: true,
569            ..Default::default()
570        };
571
572        let results = JsonComparator::compare(&actual, &expected, &options);
573        assert!(results.is_empty());
574    }
575
576    #[test]
577    fn test_redact_nested() {
578        let actual = json!({"user": {"password": "secret", "name": "test"}});
579        let expected = json!({"user": {"name": "test"}});
580
581        let options = InlineOptions {
582            redact: vec!["password".to_string()],
583            ..Default::default()
584        };
585
586        let results = JsonComparator::compare(&actual, &expected, &options);
587        assert!(results.is_empty());
588    }
589
590    #[test]
591    fn test_is_protojson_default_value() {
592        assert!(is_protojson_default_value(&Value::String("".to_string())));
593        assert!(is_protojson_default_value(&Value::Number(0.into())));
594        assert!(is_protojson_default_value(&Value::Bool(false)));
595        assert!(is_protojson_default_value(&Value::Array(vec![])));
596        assert!(is_protojson_default_value(&Value::Object(
597            serde_json::Map::new()
598        )));
599        assert!(!is_protojson_default_value(&Value::String(
600            "not empty".to_string()
601        )));
602        assert!(!is_protojson_default_value(&Value::Number(1.into())));
603        assert!(!is_protojson_default_value(&Value::Bool(true)));
604    }
605
606    #[test]
607    fn test_unordered_arrays_optimized() {
608        let actual = json!([3, 1, 2]);
609        let expected = json!([1, 2, 3]);
610        let options = InlineOptions {
611            unordered_arrays: true,
612            ..Default::default()
613        };
614
615        let results = JsonComparator::compare(&actual, &expected, &options);
616        assert!(results.is_empty());
617    }
618
619    #[test]
620    fn test_unordered_arrays_with_objects() {
621        let actual = json!([
622            {"id": 3, "name": "c"},
623            {"id": 1, "name": "a"},
624            {"id": 2, "name": "b"}
625        ]);
626        let expected = json!([
627            {"id": 1, "name": "a"},
628            {"id": 2, "name": "b"},
629            {"id": 3, "name": "c"}
630        ]);
631        let options = InlineOptions {
632            unordered_arrays: true,
633            ..Default::default()
634        };
635
636        let results = JsonComparator::compare(&actual, &expected, &options);
637        assert!(results.is_empty());
638    }
639
640    #[test]
641    fn test_unordered_arrays_missing_item() {
642        let actual = json!([1, 2]);
643        let expected = json!([1, 2, 3]);
644        let options = InlineOptions {
645            unordered_arrays: true,
646            ..Default::default()
647        };
648
649        let results = JsonComparator::compare(&actual, &expected, &options);
650        assert!(!results.is_empty());
651    }
652
653    #[test]
654    fn test_unordered_arrays_extra_item() {
655        let actual = json!([1, 2, 3, 4]);
656        let expected = json!([1, 2, 3]);
657        let options = InlineOptions {
658            unordered_arrays: true,
659            ..Default::default()
660        };
661
662        let results = JsonComparator::compare(&actual, &expected, &options);
663        assert!(!results.is_empty());
664    }
665
666    #[test]
667    fn test_unordered_arrays_partial() {
668        let actual = json!([1, 2, 3, 4]);
669        let expected = json!([1, 3]);
670        let options = InlineOptions {
671            unordered_arrays: true,
672            partial: true,
673            ..Default::default()
674        };
675
676        let results = JsonComparator::compare(&actual, &expected, &options);
677        assert!(results.is_empty());
678
679        // Partial only allows extra actual items — a missing expected item
680        // must still fail (regression: this used to pass vacuously).
681        let expected = json!([1, 999]);
682        let results = JsonComparator::compare(&actual, &expected, &options);
683        assert!(
684            !results.is_empty(),
685            "missing expected item must fail even in partial mode"
686        );
687    }
688
689    #[test]
690    fn test_unordered_arrays_partial_missing_item_fails() {
691        // Regression: with unordered_arrays + partial, expected items that are
692        // completely absent from actual used to be silently accepted.
693        let actual = json!([1, 2, 3]);
694        let expected = json!([999]);
695        let options = InlineOptions {
696            unordered_arrays: true,
697            partial: true,
698            ..Default::default()
699        };
700
701        let results = JsonComparator::compare(&actual, &expected, &options);
702        assert_eq!(results.len(), 1);
703        if let AssertionResult::Fail { message, .. } = &results[0] {
704            assert!(message.contains("Missing expected item"));
705        } else {
706            panic!("Expected Fail");
707        }
708    }
709
710    #[test]
711    fn test_unordered_arrays_numeric_representation() {
712        // Regression: hash prefilter rejected fuzzy-equal numbers (60.0 vs 60)
713        // because their hashes differ.
714        let actual = json!([60.0]);
715        let expected = json!([60]);
716        let options = InlineOptions {
717            unordered_arrays: true,
718            ..Default::default()
719        };
720
721        let results = JsonComparator::compare(&actual, &expected, &options);
722        assert!(results.is_empty(), "got: {:?}", results);
723    }
724
725    #[test]
726    fn test_unordered_arrays_wildcard() {
727        // Regression: wildcard "*" items never hash-matched anything.
728        let actual = json!(["abc", 1]);
729        let expected = json!([1, "*"]);
730        let options = InlineOptions {
731            unordered_arrays: true,
732            ..Default::default()
733        };
734
735        let results = JsonComparator::compare(&actual, &expected, &options);
736        assert!(results.is_empty(), "got: {:?}", results);
737    }
738
739    #[test]
740    fn test_unordered_arrays_tolerance() {
741        // Regression: tolerance matching inside unordered arrays was defeated
742        // by the hash prefilter.
743        let actual = json!([10.005, 20.0]);
744        let expected = json!([20.0, 10.0]);
745        let options = InlineOptions {
746            unordered_arrays: true,
747            tolerance: Some(0.01),
748            ..Default::default()
749        };
750
751        let results = JsonComparator::compare(&actual, &expected, &options);
752        assert!(results.is_empty(), "got: {:?}", results);
753    }
754
755    #[test]
756    fn test_unordered_arrays_partial_objects() {
757        // Regression: partial object matching inside unordered arrays was
758        // defeated by the hash prefilter (extra keys change the hash).
759        let actual = json!([{"id": 2, "extra": "y"}, {"id": 1, "extra": "x"}]);
760        let expected = json!([{"id": 1}, {"id": 2}]);
761        let options = InlineOptions {
762            unordered_arrays: true,
763            partial: true,
764            ..Default::default()
765        };
766
767        let results = JsonComparator::compare(&actual, &expected, &options);
768        assert!(results.is_empty(), "got: {:?}", results);
769    }
770
771    #[test]
772    fn test_compare_large_integers_exact() {
773        // Regression: i64 values that differ by 1 near i64::MAX used to pass
774        // because equality went through lossy f64 conversion.
775        let actual = json!({"id": 9223372036854775807i64});
776        let expected = json!({"id": 9223372036854775806i64});
777        let options = InlineOptions::default();
778
779        let results = JsonComparator::compare(&actual, &expected, &options);
780        assert_eq!(results.len(), 1);
781
782        let expected = json!({"id": 9223372036854775807i64});
783        let results = JsonComparator::compare(&actual, &expected, &options);
784        assert!(results.is_empty());
785    }
786
787    #[test]
788    fn test_compare_small_floats_not_equal() {
789        // Regression: fixed absolute epsilon 1e-6 made 0.0000001 == 0.0000009.
790        let actual = json!({"val": 0.0000001});
791        let expected = json!({"val": 0.0000009});
792        let options = InlineOptions::default();
793
794        let results = JsonComparator::compare(&actual, &expected, &options);
795        assert_eq!(results.len(), 1);
796    }
797
798    #[test]
799    fn test_compare_float_rounding_noise_equal() {
800        // f64 rounding noise (0.1 + 0.2 != 0.3 exactly) must still compare equal.
801        let actual = json!({"val": 0.1 + 0.2});
802        let expected = json!({"val": 0.3});
803        let options = InlineOptions::default();
804
805        let results = JsonComparator::compare(&actual, &expected, &options);
806        assert!(results.is_empty(), "got: {:?}", results);
807    }
808
809    #[test]
810    fn test_compare_mixed_sign_large_integers() {
811        // Negative i64 vs u64 beyond i64::MAX must not be equal.
812        let actual = json!({"val": -1i64});
813        let expected = json!({"val": 18446744073709551615u64});
814        let options = InlineOptions::default();
815
816        let results = JsonComparator::compare(&actual, &expected, &options);
817        assert_eq!(results.len(), 1);
818    }
819
820    #[test]
821    fn test_hash_value_consistency() {
822        let value1 = json!({"id": 1, "name": "test"});
823        let value2 = json!({"id": 1, "name": "test"});
824        let value3 = json!({"name": "test", "id": 1}); // Different key order
825
826        // Hash should be the same for equal values (including different key order)
827        let hash1 = JsonComparator::hash_value(&value1);
828        let hash2 = JsonComparator::hash_value(&value2);
829        let hash3 = JsonComparator::hash_value(&value3);
830
831        assert_eq!(hash1, hash2);
832        assert_eq!(hash2, hash3);
833    }
834}