lnmp_codec/
encoder.rs

1//! Encoder for converting structured records into LNMP text format.
2
3use crate::config::EncoderConfig;
4use lnmp_core::checksum::SemanticChecksum;
5use lnmp_core::{LnmpField, LnmpRecord, LnmpValue, TypeHint};
6
7/// Encodes a quantized embedding into compact text format
8///
9/// Format: `QV[scheme,scale,zp,min,hex_data]`
10/// Example: `QV[QInt8,0.001568,0,-0.5,a1b2c3d4...]`
11fn encode_quantized_embedding(qv: &lnmp_quant::QuantizedVector) -> String {
12    use std::fmt::Write;
13    let mut result = String::with_capacity(32 + qv.data.len() * 2);
14
15    // Format: QV[scheme,scale,zero_point,min_val,data_hex]
16    write!(
17        &mut result,
18        "QV[{:?},{},{},{},",
19        qv.scheme, qv.scale, qv.zero_point, qv.min_val
20    )
21    .unwrap();
22
23    // Append hex-encoded data
24    for byte in &qv.data {
25        write!(&mut result, "{:02x}", byte).unwrap();
26    }
27
28    result.push(']');
29    result
30}
31
32/// Encoder for LNMP text format
33pub struct Encoder {
34    use_semicolons: bool,
35    config: EncoderConfig,
36    normalizer: Option<crate::normalizer::ValueNormalizer>,
37}
38
39impl Encoder {
40    /// Creates a new encoder with default settings (canonical format)
41    pub fn new() -> Self {
42        Self {
43            // Canonical format uses newlines between top-level fields
44            use_semicolons: false,
45            config: EncoderConfig::default(),
46            normalizer: None,
47        }
48    }
49
50    /// Creates a new encoder with custom configuration
51    pub fn with_config(config: EncoderConfig) -> Self {
52        let normalizer = config.semantic_dictionary.as_ref().map(|dict| {
53            crate::normalizer::ValueNormalizer::new(crate::normalizer::NormalizationConfig {
54                semantic_dictionary: Some(dict.clone()),
55                ..crate::normalizer::NormalizationConfig::default()
56            })
57        });
58
59        Self {
60            // If canonical is enabled, prefer newlines; otherwise use semicolons for inline format
61            use_semicolons: !config.canonical,
62            config,
63            normalizer,
64        }
65    }
66
67    /// Creates a new encoder with specified format (deprecated - use new() for canonical format)
68    #[deprecated(
69        note = "Use new() for canonical format. Semicolons are not part of v0.2 canonical format."
70    )]
71    pub fn with_semicolons(use_semicolons: bool) -> Self {
72        Self {
73            use_semicolons,
74            config: EncoderConfig::default(),
75            normalizer: None,
76        }
77    }
78
79    /// Encodes a complete record into LNMP text format (canonical format with sorted fields)
80    pub fn encode(&self, record: &LnmpRecord) -> String {
81        // Canonicalize the record first (sorts fields and nested structures)
82        let canonical = canonicalize_record(record);
83
84        let fields: Vec<String> = canonical
85            .fields()
86            .iter()
87            .map(|field| {
88                let normalized_value = if let Some(norm) = &self.normalizer {
89                    norm.normalize_with_fid(Some(field.fid), &field.value)
90                } else {
91                    field.value.clone()
92                };
93                let normalized_field = LnmpField {
94                    fid: field.fid,
95                    value: normalized_value,
96                };
97                self.encode_field(&normalized_field)
98            })
99            .collect();
100
101        if self.use_semicolons {
102            fields.join(";")
103        } else {
104            fields.join("\n")
105        }
106    }
107
108    /// Encodes a single field (F<fid>=<value> or F<fid>:<type>=<value> or with checksum)
109    fn encode_field(&self, field: &LnmpField) -> String {
110        let type_hint = if self.config.include_type_hints {
111            Some(self.get_type_hint(&field.value))
112        } else {
113            None
114        };
115
116        let base = if let Some(hint) = type_hint {
117            format!(
118                "F{}:{}={}",
119                field.fid,
120                hint.as_str(),
121                self.encode_value(&field.value)
122            )
123        } else {
124            format!("F{}={}", field.fid, self.encode_value(&field.value))
125        };
126
127        // Append checksum if enabled
128        if self.config.enable_checksums {
129            let checksum = SemanticChecksum::compute(field.fid, type_hint, &field.value);
130            let checksum_str = SemanticChecksum::format(checksum);
131            format!("{}#{}", base, checksum_str)
132        } else {
133            base
134        }
135    }
136
137    /// Gets the type hint for a value
138    fn get_type_hint(&self, value: &LnmpValue) -> TypeHint {
139        match value {
140            LnmpValue::Int(_) => TypeHint::Int,
141            LnmpValue::Float(_) => TypeHint::Float,
142            LnmpValue::Bool(_) => TypeHint::Bool,
143            LnmpValue::String(_) => TypeHint::String,
144            LnmpValue::StringArray(_) => TypeHint::StringArray,
145            LnmpValue::IntArray(_) => TypeHint::IntArray,
146            LnmpValue::FloatArray(_) => TypeHint::FloatArray,
147            LnmpValue::BoolArray(_) => TypeHint::BoolArray,
148            LnmpValue::NestedRecord(_) => TypeHint::Record,
149            LnmpValue::NestedArray(_) => TypeHint::RecordArray,
150            LnmpValue::Embedding(_) => TypeHint::Embedding,
151            LnmpValue::EmbeddingDelta(_) => TypeHint::Embedding,
152            LnmpValue::QuantizedEmbedding(_) => TypeHint::QuantizedEmbedding,
153        }
154    }
155
156    /// Encodes a value based on its type
157    fn encode_value(&self, value: &LnmpValue) -> String {
158        match value {
159            LnmpValue::Int(i) => i.to_string(),
160            LnmpValue::Float(f) => f.to_string(),
161            LnmpValue::Bool(b) => {
162                if *b {
163                    "1".to_string()
164                } else {
165                    "0".to_string()
166                }
167            }
168            LnmpValue::String(s) => self.encode_string(s),
169            LnmpValue::StringArray(arr) => {
170                // Canonical format: no spaces after commas
171                let items: Vec<String> = arr.iter().map(|s| self.encode_string(s)).collect();
172                format!("[{}]", items.join(","))
173            }
174            LnmpValue::IntArray(arr) => {
175                let items: Vec<String> = arr.iter().map(|i| i.to_string()).collect();
176                format!("[{}]", items.join(","))
177            }
178            LnmpValue::FloatArray(arr) => {
179                let items: Vec<String> = arr.iter().map(|f| f.to_string()).collect();
180                format!("[{}]", items.join(","))
181            }
182            LnmpValue::BoolArray(arr) => {
183                let items: Vec<String> = arr
184                    .iter()
185                    .map(|b| if *b { "1".to_string() } else { "0".to_string() })
186                    .collect();
187                format!("[{}]", items.join(","))
188            }
189            LnmpValue::NestedRecord(record) => self.encode_nested_record(record),
190            LnmpValue::NestedArray(records) => self.encode_nested_array(records),
191            LnmpValue::Embedding(vec) => {
192                // Text format representation for embeddings is not yet standardized.
193                // We use a placeholder format that indicates the dimension.
194                format!("[vector dim={}]", vec.dim)
195            }
196            LnmpValue::EmbeddingDelta(delta) => {
197                // Text format representation for embedding deltas is not yet standardized.
198                // We use a placeholder format that indicates the number of changes.
199                format!("[vector_delta changes={}]", delta.changes.len())
200            }
201            LnmpValue::QuantizedEmbedding(qv) => {
202                // Compact text format: QV[scheme,scale,zp,min,hex_data]
203                encode_quantized_embedding(qv)
204            }
205        }
206    }
207
208    /// Encodes a nested record {F<fid>=<value>;F<fid>=<value>}
209    fn encode_nested_record(&self, record: &LnmpRecord) -> String {
210        if record.fields().is_empty() {
211            return "{}".to_string();
212        }
213
214        let fields: Vec<String> = record
215            .fields()
216            .iter()
217            .map(|field| self.encode_field_without_checksum(field))
218            .collect();
219
220        // Nested records always use semicolons as separators
221        format!("{{{}}}", fields.join(";"))
222    }
223
224    /// Encodes a field without checksum (for use in nested structures)
225    fn encode_field_without_checksum(&self, field: &LnmpField) -> String {
226        if self.config.include_type_hints {
227            let type_hint = self.get_type_hint(&field.value);
228            format!(
229                "F{}:{}={}",
230                field.fid,
231                type_hint.as_str(),
232                self.encode_value(&field.value)
233            )
234        } else {
235            format!("F{}={}", field.fid, self.encode_value(&field.value))
236        }
237    }
238
239    /// Encodes a nested array [{...},{...}]
240    fn encode_nested_array(&self, records: &[LnmpRecord]) -> String {
241        if records.is_empty() {
242            return "[]".to_string();
243        }
244
245        let encoded_records: Vec<String> = records
246            .iter()
247            .map(|record| self.encode_nested_record(record))
248            .collect();
249
250        // Canonical format: no spaces after commas
251        format!("[{}]", encoded_records.join(","))
252    }
253
254    /// Encodes a string, adding quotes and escapes if needed
255    fn encode_string(&self, s: &str) -> String {
256        if self.needs_quoting(s) {
257            format!("\"{}\"", self.escape_string(s))
258        } else {
259            s.to_string()
260        }
261    }
262
263    /// Checks if a string needs quoting
264    fn needs_quoting(&self, s: &str) -> bool {
265        if s.is_empty() || looks_like_literal(s) {
266            return true;
267        }
268
269        for ch in s.chars() {
270            if !is_safe_unquoted_char(ch) {
271                return true;
272            }
273        }
274
275        false
276    }
277
278    /// Escapes special characters in a string
279    fn escape_string(&self, s: &str) -> String {
280        let mut result = String::new();
281        for ch in s.chars() {
282            match ch {
283                '"' => result.push_str("\\\""),
284                '\\' => result.push_str("\\\\"),
285                '\n' => result.push_str("\\n"),
286                '\r' => result.push_str("\\r"),
287                '\t' => result.push_str("\\t"),
288                _ => result.push(ch),
289            }
290        }
291        result
292    }
293}
294
295impl Default for Encoder {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301/// Canonicalizes a record by recursively sorting fields and normalizing nested structures
302///
303/// This function ensures deterministic encoding by:
304/// - Sorting fields by FID at every nesting level (depth-first)
305/// - Recursively canonicalizing nested records and arrays
306/// - Omitting redundant empty fields (empty strings, empty arrays, empty nested structures)
307/// - Maintaining structural integrity
308pub fn canonicalize_record(record: &LnmpRecord) -> LnmpRecord {
309    let mut canonical = LnmpRecord::new();
310
311    // Sort fields by FID (stable sort preserves insertion order for duplicates)
312    let sorted = record.sorted_fields();
313
314    for field in sorted {
315        let canonical_value = canonicalize_value(&field.value);
316
317        // Omit redundant empty fields
318        if !is_empty_value(&canonical_value) {
319            canonical.add_field(LnmpField {
320                fid: field.fid,
321                value: canonical_value,
322            });
323        }
324    }
325
326    canonical
327}
328
329/// Canonicalizes a value by recursively processing nested structures
330fn canonicalize_value(value: &LnmpValue) -> LnmpValue {
331    match value {
332        // Primitive values are already canonical
333        LnmpValue::Int(i) => LnmpValue::Int(*i),
334        LnmpValue::Float(f) => LnmpValue::Float(*f),
335        LnmpValue::Bool(b) => LnmpValue::Bool(*b),
336        LnmpValue::String(s) => LnmpValue::String(s.clone()),
337        LnmpValue::StringArray(arr) => LnmpValue::StringArray(arr.clone()),
338        LnmpValue::IntArray(arr) => LnmpValue::IntArray(arr.clone()),
339        LnmpValue::FloatArray(arr) => LnmpValue::FloatArray(arr.clone()),
340        LnmpValue::BoolArray(arr) => LnmpValue::BoolArray(arr.clone()),
341
342        // Recursively canonicalize nested record
343        LnmpValue::NestedRecord(nested) => {
344            let canonical_nested = canonicalize_record(nested);
345            LnmpValue::NestedRecord(Box::new(canonical_nested))
346        }
347
348        // Recursively canonicalize each record in nested array
349        LnmpValue::NestedArray(arr) => {
350            let canonical_arr: Vec<LnmpRecord> = arr.iter().map(canonicalize_record).collect();
351            LnmpValue::NestedArray(canonical_arr)
352        }
353        // Embeddings are already canonical (binary data)
354        LnmpValue::Embedding(vec) => LnmpValue::Embedding(vec.clone()),
355        LnmpValue::EmbeddingDelta(delta) => LnmpValue::EmbeddingDelta(delta.clone()),
356        LnmpValue::QuantizedEmbedding(qv) => LnmpValue::QuantizedEmbedding(qv.clone()),
357    }
358}
359
360/// Checks if a value is considered "empty" and should be omitted during canonicalization
361///
362/// Empty values include:
363/// - Empty strings
364/// - Empty string arrays
365/// - Empty nested records (records with no fields)
366/// - Empty nested arrays (arrays with no elements)
367fn is_empty_value(value: &LnmpValue) -> bool {
368    match value {
369        LnmpValue::String(s) => s.is_empty(),
370        LnmpValue::StringArray(arr) => arr.is_empty(),
371        LnmpValue::IntArray(arr) => arr.is_empty(),
372        LnmpValue::FloatArray(arr) => arr.is_empty(),
373        LnmpValue::BoolArray(arr) => arr.is_empty(),
374        LnmpValue::NestedRecord(record) => record.fields().is_empty(),
375        LnmpValue::NestedArray(arr) => arr.is_empty(),
376        // Embeddings are never considered empty even if dimension is 0 (which shouldn't happen)
377        LnmpValue::Embedding(_) => false,
378        LnmpValue::EmbeddingDelta(_) => false,
379        LnmpValue::QuantizedEmbedding(_) => false,
380        // Non-empty primitive values are never considered empty
381        LnmpValue::Int(_) | LnmpValue::Float(_) | LnmpValue::Bool(_) => false,
382    }
383}
384
385/// Validates that canonicalization is idempotent (round-trip stable)
386///
387/// Verifies that canonicalize(canonicalize(x)) == canonicalize(x)
388/// This ensures that the canonicalization process is stable and deterministic.
389///
390/// Returns true if the record is round-trip stable, false otherwise.
391pub fn validate_round_trip_stability(record: &LnmpRecord) -> bool {
392    let canonical_once = canonicalize_record(record);
393    let canonical_twice = canonicalize_record(&canonical_once);
394    canonical_once == canonical_twice
395}
396
397/// Checks if a character is safe for unquoted strings
398fn is_safe_unquoted_char(ch: char) -> bool {
399    ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.'
400}
401
402fn looks_like_literal(s: &str) -> bool {
403    if s.trim().is_empty() {
404        return true;
405    }
406
407    let lower = s.to_ascii_lowercase();
408    matches!(lower.as_str(), "true" | "false" | "yes" | "no")
409        || s.parse::<i64>().is_ok()
410        || s.parse::<f64>().is_ok()
411}
412
413#[cfg(test)]
414#[allow(clippy::approx_constant)]
415mod tests {
416    use super::*;
417    use lnmp_core::LnmpField;
418
419    #[test]
420    fn test_encode_integer() {
421        let mut record = LnmpRecord::new();
422        record.add_field(LnmpField {
423            fid: 1,
424            value: LnmpValue::Int(42),
425        });
426
427        let encoder = Encoder::new();
428        let output = encoder.encode(&record);
429        assert_eq!(output, "F1=42");
430    }
431
432    #[test]
433    fn test_encode_negative_integer() {
434        let mut record = LnmpRecord::new();
435        record.add_field(LnmpField {
436            fid: 1,
437            value: LnmpValue::Int(-123),
438        });
439
440        let encoder = Encoder::new();
441        let output = encoder.encode(&record);
442        assert_eq!(output, "F1=-123");
443    }
444
445    #[test]
446    fn test_encode_float() {
447        let mut record = LnmpRecord::new();
448        record.add_field(LnmpField {
449            fid: 2,
450            value: LnmpValue::Float(3.14),
451        });
452
453        let encoder = Encoder::new();
454        let output = encoder.encode(&record);
455        assert_eq!(output, "F2=3.14");
456    }
457
458    #[test]
459    fn test_encode_bool_true() {
460        let mut record = LnmpRecord::new();
461        record.add_field(LnmpField {
462            fid: 3,
463            value: LnmpValue::Bool(true),
464        });
465
466        let encoder = Encoder::new();
467        let output = encoder.encode(&record);
468        assert_eq!(output, "F3=1");
469    }
470
471    #[test]
472    fn test_encode_bool_false() {
473        let mut record = LnmpRecord::new();
474        record.add_field(LnmpField {
475            fid: 3,
476            value: LnmpValue::Bool(false),
477        });
478
479        let encoder = Encoder::new();
480        let output = encoder.encode(&record);
481        assert_eq!(output, "F3=0");
482    }
483
484    #[test]
485    fn test_encode_unquoted_string() {
486        let mut record = LnmpRecord::new();
487        record.add_field(LnmpField {
488            fid: 4,
489            value: LnmpValue::String("test_value".to_string()),
490        });
491
492        let encoder = Encoder::new();
493        let output = encoder.encode(&record);
494        assert_eq!(output, "F4=test_value");
495    }
496
497    #[test]
498    fn test_encode_quoted_string() {
499        let mut record = LnmpRecord::new();
500        record.add_field(LnmpField {
501            fid: 4,
502            value: LnmpValue::String("hello world".to_string()),
503        });
504
505        let encoder = Encoder::new();
506        let output = encoder.encode(&record);
507        assert_eq!(output, r#"F4="hello world""#);
508    }
509
510    #[test]
511    fn test_encode_string_with_escapes() {
512        let mut record = LnmpRecord::new();
513        record.add_field(LnmpField {
514            fid: 4,
515            value: LnmpValue::String("hello \"world\"".to_string()),
516        });
517
518        let encoder = Encoder::new();
519        let output = encoder.encode(&record);
520        assert_eq!(output, r#"F4="hello \"world\"""#);
521    }
522
523    #[test]
524    fn test_encode_string_with_newline() {
525        let mut record = LnmpRecord::new();
526        record.add_field(LnmpField {
527            fid: 4,
528            value: LnmpValue::String("line1\nline2".to_string()),
529        });
530
531        let encoder = Encoder::new();
532        let output = encoder.encode(&record);
533        assert_eq!(output, r#"F4="line1\nline2""#);
534    }
535
536    #[test]
537    fn test_encode_string_with_backslash() {
538        let mut record = LnmpRecord::new();
539        record.add_field(LnmpField {
540            fid: 4,
541            value: LnmpValue::String("back\\slash".to_string()),
542        });
543
544        let encoder = Encoder::new();
545        let output = encoder.encode(&record);
546        assert_eq!(output, r#"F4="back\\slash""#);
547    }
548
549    #[test]
550    fn test_encode_string_array() {
551        let mut record = LnmpRecord::new();
552        record.add_field(LnmpField {
553            fid: 5,
554            value: LnmpValue::StringArray(vec![
555                "admin".to_string(),
556                "dev".to_string(),
557                "user".to_string(),
558            ]),
559        });
560
561        let encoder = Encoder::new();
562        let output = encoder.encode(&record);
563        assert_eq!(output, "F5=[admin,dev,user]");
564    }
565
566    #[test]
567    fn test_encode_string_array_with_quoted_strings() {
568        let mut record = LnmpRecord::new();
569        record.add_field(LnmpField {
570            fid: 5,
571            value: LnmpValue::StringArray(vec!["hello world".to_string(), "test".to_string()]),
572        });
573
574        let encoder = Encoder::new();
575        let output = encoder.encode(&record);
576        assert_eq!(output, r#"F5=["hello world",test]"#);
577    }
578
579    #[test]
580    fn test_encode_empty_string_array() {
581        // Empty string arrays are omitted during canonicalization (Requirement 9.3)
582        let mut record = LnmpRecord::new();
583        record.add_field(LnmpField {
584            fid: 5,
585            value: LnmpValue::StringArray(vec![]),
586        });
587
588        let encoder = Encoder::new();
589        let output = encoder.encode(&record);
590        assert_eq!(output, ""); // Empty field is omitted
591    }
592
593    #[test]
594    fn test_encode_multiline_format() {
595        let mut record = LnmpRecord::new();
596        record.add_field(LnmpField {
597            fid: 12,
598            value: LnmpValue::Int(14532),
599        });
600        record.add_field(LnmpField {
601            fid: 7,
602            value: LnmpValue::Bool(true),
603        });
604        record.add_field(LnmpField {
605            fid: 20,
606            value: LnmpValue::String("Halil".to_string()),
607        });
608
609        let encoder = Encoder::new();
610        let output = encoder.encode(&record);
611        // Canonical format: fields are sorted by FID
612        assert_eq!(output, "F7=1\nF12=14532\nF20=Halil");
613    }
614
615    #[test]
616    #[allow(deprecated)]
617    fn test_encode_inline_format() {
618        let mut record = LnmpRecord::new();
619        record.add_field(LnmpField {
620            fid: 12,
621            value: LnmpValue::Int(14532),
622        });
623        record.add_field(LnmpField {
624            fid: 7,
625            value: LnmpValue::Bool(true),
626        });
627        record.add_field(LnmpField {
628            fid: 23,
629            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
630        });
631
632        let encoder = Encoder::with_semicolons(true);
633        let output = encoder.encode(&record);
634        // Fields are sorted even with semicolons
635        assert_eq!(output, "F7=1;F12=14532;F23=[admin,dev]");
636    }
637
638    #[test]
639    fn test_encode_empty_record() {
640        let record = LnmpRecord::new();
641        let encoder = Encoder::new();
642        let output = encoder.encode(&record);
643        assert_eq!(output, "");
644    }
645
646    #[test]
647    #[allow(deprecated)]
648    fn test_encode_spec_example() {
649        let mut record = LnmpRecord::new();
650        record.add_field(LnmpField {
651            fid: 12,
652            value: LnmpValue::Int(14532),
653        });
654        record.add_field(LnmpField {
655            fid: 7,
656            value: LnmpValue::Bool(true),
657        });
658        record.add_field(LnmpField {
659            fid: 23,
660            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
661        });
662
663        let encoder = Encoder::with_semicolons(true);
664        let output = encoder.encode(&record);
665        // Fields are sorted by FID
666        assert_eq!(output, "F7=1;F12=14532;F23=[admin,dev]");
667    }
668
669    #[test]
670    fn test_needs_quoting() {
671        let encoder = Encoder::new();
672
673        assert!(!encoder.needs_quoting("simple"));
674        assert!(!encoder.needs_quoting("test_value"));
675        assert!(!encoder.needs_quoting("file-name"));
676        assert!(!encoder.needs_quoting("version1.2.3"));
677
678        assert!(encoder.needs_quoting("hello world"));
679        assert!(encoder.needs_quoting("test@example"));
680        assert!(encoder.needs_quoting(""));
681        assert!(encoder.needs_quoting("test;value"));
682    }
683
684    #[test]
685    fn test_escape_string() {
686        let encoder = Encoder::new();
687
688        assert_eq!(encoder.escape_string("simple"), "simple");
689        assert_eq!(
690            encoder.escape_string("hello \"world\""),
691            r#"hello \"world\""#
692        );
693        assert_eq!(encoder.escape_string("back\\slash"), r#"back\\slash"#);
694        assert_eq!(encoder.escape_string("line1\nline2"), r#"line1\nline2"#);
695        assert_eq!(encoder.escape_string("tab\there"), r#"tab\there"#);
696        assert_eq!(encoder.escape_string("return\rhere"), r#"return\rhere"#);
697    }
698
699    #[test]
700    #[allow(deprecated)]
701    fn test_round_trip() {
702        use crate::parser::Parser;
703
704        let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
705        let mut parser = Parser::new(input).unwrap();
706        let record = parser.parse_record().unwrap();
707
708        let encoder = Encoder::with_semicolons(true);
709        let output = encoder.encode(&record);
710
711        // Parse again to verify
712        let mut parser2 = Parser::new(&output).unwrap();
713        let record2 = parser2.parse_record().unwrap();
714
715        assert_eq!(record.fields().len(), record2.fields().len());
716        assert_eq!(
717            record.get_field(12).unwrap().value,
718            record2.get_field(12).unwrap().value
719        );
720        assert_eq!(
721            record.get_field(7).unwrap().value,
722            record2.get_field(7).unwrap().value
723        );
724        assert_eq!(
725            record.get_field(23).unwrap().value,
726            record2.get_field(23).unwrap().value
727        );
728    }
729
730    #[test]
731    fn test_deterministic_field_sorting() {
732        // Create record with fields in random order
733        let mut record = LnmpRecord::new();
734        record.add_field(LnmpField {
735            fid: 100,
736            value: LnmpValue::Int(3),
737        });
738        record.add_field(LnmpField {
739            fid: 5,
740            value: LnmpValue::Int(1),
741        });
742        record.add_field(LnmpField {
743            fid: 50,
744            value: LnmpValue::Int(2),
745        });
746
747        let encoder = Encoder::new();
748        let output = encoder.encode(&record);
749
750        // Fields should be sorted by FID
751        assert_eq!(output, "F5=1\nF50=2\nF100=3");
752    }
753
754    #[test]
755    fn test_deterministic_sorting_with_duplicates() {
756        // Test that stable sort preserves insertion order for duplicate FIDs
757        let mut record = LnmpRecord::new();
758        record.add_field(LnmpField {
759            fid: 10,
760            value: LnmpValue::String("first".to_string()),
761        });
762        record.add_field(LnmpField {
763            fid: 5,
764            value: LnmpValue::Int(1),
765        });
766        record.add_field(LnmpField {
767            fid: 10,
768            value: LnmpValue::String("second".to_string()),
769        });
770
771        let encoder = Encoder::new();
772        let output = encoder.encode(&record);
773
774        // F5 first, then both F10s in insertion order
775        assert_eq!(output, "F5=1\nF10=first\nF10=second");
776    }
777
778    #[test]
779    fn test_canonical_whitespace_formatting() {
780        // Test that there's no whitespace around equals signs
781        let mut record = LnmpRecord::new();
782        record.add_field(LnmpField {
783            fid: 1,
784            value: LnmpValue::Int(42),
785        });
786
787        let encoder = Encoder::new();
788        let output = encoder.encode(&record);
789
790        // No spaces around =
791        assert_eq!(output, "F1=42");
792        assert!(!output.contains(" = "));
793        assert!(!output.contains("= "));
794        assert!(!output.contains(" ="));
795    }
796
797    #[test]
798    fn test_array_formatting_no_spaces() {
799        // Test that arrays have no spaces after commas
800        let mut record = LnmpRecord::new();
801        record.add_field(LnmpField {
802            fid: 1,
803            value: LnmpValue::StringArray(vec![
804                "one".to_string(),
805                "two".to_string(),
806                "three".to_string(),
807            ]),
808        });
809
810        let encoder = Encoder::new();
811        let output = encoder.encode(&record);
812
813        // No spaces after commas in array
814        assert_eq!(output, "F1=[one,two,three]");
815        assert!(!output.contains(", "));
816    }
817
818    #[test]
819    fn test_encoding_with_type_hints() {
820        use crate::config::EncoderConfig;
821
822        let mut record = LnmpRecord::new();
823        record.add_field(LnmpField {
824            fid: 12,
825            value: LnmpValue::Int(14532),
826        });
827        record.add_field(LnmpField {
828            fid: 7,
829            value: LnmpValue::Bool(true),
830        });
831        record.add_field(LnmpField {
832            fid: 5,
833            value: LnmpValue::Float(3.14),
834        });
835
836        let config = EncoderConfig::new().with_type_hints(true);
837        let encoder = Encoder::with_config(config);
838        let output = encoder.encode(&record);
839
840        // Fields sorted with type hints
841        assert_eq!(output, "F5:f=3.14\nF7:b=1\nF12:i=14532");
842    }
843
844    #[test]
845    fn test_encoding_without_type_hints() {
846        use crate::config::EncoderConfig;
847
848        let mut record = LnmpRecord::new();
849        record.add_field(LnmpField {
850            fid: 12,
851            value: LnmpValue::Int(14532),
852        });
853        record.add_field(LnmpField {
854            fid: 7,
855            value: LnmpValue::Bool(true),
856        });
857
858        let config = EncoderConfig::new();
859        let encoder = Encoder::with_config(config);
860        let output = encoder.encode(&record);
861
862        // Fields sorted without type hints
863        assert_eq!(output, "F7=1\nF12=14532");
864        assert!(!output.contains(':'));
865    }
866
867    #[test]
868    fn test_all_type_hints() {
869        use crate::config::EncoderConfig;
870
871        let mut record = LnmpRecord::new();
872        record.add_field(LnmpField {
873            fid: 1,
874            value: LnmpValue::Int(42),
875        });
876        record.add_field(LnmpField {
877            fid: 2,
878            value: LnmpValue::Float(3.14),
879        });
880        record.add_field(LnmpField {
881            fid: 3,
882            value: LnmpValue::Bool(true),
883        });
884        record.add_field(LnmpField {
885            fid: 4,
886            value: LnmpValue::String("test".to_string()),
887        });
888        record.add_field(LnmpField {
889            fid: 5,
890            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
891        });
892
893        let config = EncoderConfig::new().with_type_hints(true);
894
895        let encoder = Encoder::with_config(config);
896        let output = encoder.encode(&record);
897
898        assert_eq!(output, "F1:i=42\nF2:f=3.14\nF3:b=1\nF4:s=test\nF5:sa=[a,b]");
899    }
900
901    #[test]
902    fn test_multiple_encode_cycles_identical() {
903        // Test that encoding multiple times produces identical output
904        let mut record = LnmpRecord::new();
905        record.add_field(LnmpField {
906            fid: 23,
907            value: LnmpValue::Int(3),
908        });
909        record.add_field(LnmpField {
910            fid: 7,
911            value: LnmpValue::Int(2),
912        });
913        record.add_field(LnmpField {
914            fid: 12,
915            value: LnmpValue::Int(1),
916        });
917
918        let encoder = Encoder::new();
919        let output1 = encoder.encode(&record);
920        let output2 = encoder.encode(&record);
921        let output3 = encoder.encode(&record);
922
923        assert_eq!(output1, output2);
924        assert_eq!(output2, output3);
925    }
926
927    #[test]
928    fn test_canonicalize_record_basic() {
929        // Test basic field sorting
930        let mut record = LnmpRecord::new();
931        record.add_field(LnmpField {
932            fid: 100,
933            value: LnmpValue::Int(3),
934        });
935        record.add_field(LnmpField {
936            fid: 5,
937            value: LnmpValue::Int(1),
938        });
939        record.add_field(LnmpField {
940            fid: 50,
941            value: LnmpValue::Int(2),
942        });
943
944        let canonical = canonicalize_record(&record);
945        let fields = canonical.fields();
946
947        assert_eq!(fields.len(), 3);
948        assert_eq!(fields[0].fid, 5);
949        assert_eq!(fields[1].fid, 50);
950        assert_eq!(fields[2].fid, 100);
951    }
952
953    #[test]
954    fn test_canonicalize_record_with_nested_record() {
955        // Test nested record canonicalization
956        let mut inner_record = LnmpRecord::new();
957        inner_record.add_field(LnmpField {
958            fid: 12,
959            value: LnmpValue::Int(1),
960        });
961        inner_record.add_field(LnmpField {
962            fid: 7,
963            value: LnmpValue::Bool(true),
964        });
965
966        let mut outer_record = LnmpRecord::new();
967        outer_record.add_field(LnmpField {
968            fid: 50,
969            value: LnmpValue::NestedRecord(Box::new(inner_record)),
970        });
971        outer_record.add_field(LnmpField {
972            fid: 10,
973            value: LnmpValue::String("test".to_string()),
974        });
975
976        let canonical = canonicalize_record(&outer_record);
977        let fields = canonical.fields();
978
979        // Outer fields should be sorted
980        assert_eq!(fields.len(), 2);
981        assert_eq!(fields[0].fid, 10);
982        assert_eq!(fields[1].fid, 50);
983
984        // Inner fields should also be sorted
985        if let LnmpValue::NestedRecord(nested) = &fields[1].value {
986            let nested_fields = nested.fields();
987            assert_eq!(nested_fields.len(), 2);
988            assert_eq!(nested_fields[0].fid, 7);
989            assert_eq!(nested_fields[1].fid, 12);
990        } else {
991            panic!("Expected nested record");
992        }
993    }
994
995    #[test]
996    fn test_canonicalize_record_with_nested_array() {
997        // Test nested array canonicalization
998        let mut record1 = LnmpRecord::new();
999        record1.add_field(LnmpField {
1000            fid: 20,
1001            value: LnmpValue::Int(2),
1002        });
1003        record1.add_field(LnmpField {
1004            fid: 10,
1005            value: LnmpValue::Int(1),
1006        });
1007
1008        let mut record2 = LnmpRecord::new();
1009        record2.add_field(LnmpField {
1010            fid: 30,
1011            value: LnmpValue::Int(4),
1012        });
1013        record2.add_field(LnmpField {
1014            fid: 15,
1015            value: LnmpValue::Int(3),
1016        });
1017
1018        let mut outer_record = LnmpRecord::new();
1019        outer_record.add_field(LnmpField {
1020            fid: 60,
1021            value: LnmpValue::NestedArray(vec![record1, record2]),
1022        });
1023
1024        let canonical = canonicalize_record(&outer_record);
1025        let fields = canonical.fields();
1026
1027        assert_eq!(fields.len(), 1);
1028        assert_eq!(fields[0].fid, 60);
1029
1030        // Each record in the array should have sorted fields
1031        if let LnmpValue::NestedArray(arr) = &fields[0].value {
1032            assert_eq!(arr.len(), 2);
1033
1034            let arr_fields1 = arr[0].fields();
1035            assert_eq!(arr_fields1[0].fid, 10);
1036            assert_eq!(arr_fields1[1].fid, 20);
1037
1038            let arr_fields2 = arr[1].fields();
1039            assert_eq!(arr_fields2[0].fid, 15);
1040            assert_eq!(arr_fields2[1].fid, 30);
1041        } else {
1042            panic!("Expected nested array");
1043        }
1044    }
1045
1046    #[test]
1047    fn test_canonicalize_deeply_nested_structure() {
1048        // Test 3-level deep nesting
1049        let mut level3 = LnmpRecord::new();
1050        level3.add_field(LnmpField {
1051            fid: 3,
1052            value: LnmpValue::Int(3),
1053        });
1054        level3.add_field(LnmpField {
1055            fid: 1,
1056            value: LnmpValue::Int(1),
1057        });
1058
1059        let mut level2 = LnmpRecord::new();
1060        level2.add_field(LnmpField {
1061            fid: 20,
1062            value: LnmpValue::NestedRecord(Box::new(level3)),
1063        });
1064        level2.add_field(LnmpField {
1065            fid: 10,
1066            value: LnmpValue::Int(2),
1067        });
1068
1069        let mut level1 = LnmpRecord::new();
1070        level1.add_field(LnmpField {
1071            fid: 100,
1072            value: LnmpValue::NestedRecord(Box::new(level2)),
1073        });
1074        level1.add_field(LnmpField {
1075            fid: 50,
1076            value: LnmpValue::String("test".to_string()),
1077        });
1078
1079        let canonical = canonicalize_record(&level1);
1080        let fields = canonical.fields();
1081
1082        // Level 1 should be sorted
1083        assert_eq!(fields[0].fid, 50);
1084        assert_eq!(fields[1].fid, 100);
1085
1086        // Level 2 should be sorted
1087        if let LnmpValue::NestedRecord(level2_rec) = &fields[1].value {
1088            let level2_fields = level2_rec.fields();
1089            assert_eq!(level2_fields[0].fid, 10);
1090            assert_eq!(level2_fields[1].fid, 20);
1091
1092            // Level 3 should be sorted
1093            if let LnmpValue::NestedRecord(level3_rec) = &level2_fields[1].value {
1094                let level3_fields = level3_rec.fields();
1095                assert_eq!(level3_fields[0].fid, 1);
1096                assert_eq!(level3_fields[1].fid, 3);
1097            } else {
1098                panic!("Expected level 3 nested record");
1099            }
1100        } else {
1101            panic!("Expected level 2 nested record");
1102        }
1103    }
1104
1105    #[test]
1106    fn test_canonicalize_empty_record() {
1107        let record = LnmpRecord::new();
1108        let canonical = canonicalize_record(&record);
1109        assert_eq!(canonical.fields().len(), 0);
1110    }
1111
1112    #[test]
1113    fn test_canonicalize_empty_nested_structures() {
1114        // Empty nested structures should be omitted during canonicalization (Requirement 9.3)
1115        let mut record = LnmpRecord::new();
1116        record.add_field(LnmpField {
1117            fid: 50,
1118            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())),
1119        });
1120        record.add_field(LnmpField {
1121            fid: 60,
1122            value: LnmpValue::NestedArray(vec![]),
1123        });
1124
1125        let canonical = canonicalize_record(&record);
1126        let fields = canonical.fields();
1127
1128        // Empty nested structures should be omitted
1129        assert_eq!(fields.len(), 0);
1130    }
1131
1132    #[test]
1133    fn test_canonicalize_preserves_values() {
1134        // Test that canonicalization doesn't modify values
1135        let mut record = LnmpRecord::new();
1136        record.add_field(LnmpField {
1137            fid: 1,
1138            value: LnmpValue::Int(42),
1139        });
1140        record.add_field(LnmpField {
1141            fid: 2,
1142            value: LnmpValue::Float(3.14159),
1143        });
1144        record.add_field(LnmpField {
1145            fid: 3,
1146            value: LnmpValue::Bool(true),
1147        });
1148        record.add_field(LnmpField {
1149            fid: 4,
1150            value: LnmpValue::String("test value".to_string()),
1151        });
1152        record.add_field(LnmpField {
1153            fid: 5,
1154            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
1155        });
1156
1157        let canonical = canonicalize_record(&record);
1158
1159        assert_eq!(canonical.get_field(1).unwrap().value, LnmpValue::Int(42));
1160        assert_eq!(
1161            canonical.get_field(2).unwrap().value,
1162            LnmpValue::Float(3.14159)
1163        );
1164        assert_eq!(canonical.get_field(3).unwrap().value, LnmpValue::Bool(true));
1165        assert_eq!(
1166            canonical.get_field(4).unwrap().value,
1167            LnmpValue::String("test value".to_string())
1168        );
1169        assert_eq!(
1170            canonical.get_field(5).unwrap().value,
1171            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1172        );
1173    }
1174
1175    #[test]
1176    fn test_canonicalize_idempotent() {
1177        // Test that canonicalizing twice produces the same result
1178        let mut record = LnmpRecord::new();
1179        record.add_field(LnmpField {
1180            fid: 100,
1181            value: LnmpValue::Int(3),
1182        });
1183        record.add_field(LnmpField {
1184            fid: 5,
1185            value: LnmpValue::Int(1),
1186        });
1187        record.add_field(LnmpField {
1188            fid: 50,
1189            value: LnmpValue::Int(2),
1190        });
1191
1192        let canonical1 = canonicalize_record(&record);
1193        let canonical2 = canonicalize_record(&canonical1);
1194
1195        assert_eq!(canonical1, canonical2);
1196    }
1197
1198    #[test]
1199    fn test_canonicalize_mixed_nested_structures() {
1200        // Test record with both nested records and nested arrays
1201        let mut inner_record = LnmpRecord::new();
1202        inner_record.add_field(LnmpField {
1203            fid: 15,
1204            value: LnmpValue::Int(5),
1205        });
1206        inner_record.add_field(LnmpField {
1207            fid: 5,
1208            value: LnmpValue::Int(3),
1209        });
1210
1211        let mut array_record = LnmpRecord::new();
1212        array_record.add_field(LnmpField {
1213            fid: 25,
1214            value: LnmpValue::Int(7),
1215        });
1216        array_record.add_field(LnmpField {
1217            fid: 20,
1218            value: LnmpValue::Int(6),
1219        });
1220
1221        let mut outer_record = LnmpRecord::new();
1222        outer_record.add_field(LnmpField {
1223            fid: 100,
1224            value: LnmpValue::NestedRecord(Box::new(inner_record)),
1225        });
1226        outer_record.add_field(LnmpField {
1227            fid: 50,
1228            value: LnmpValue::NestedArray(vec![array_record]),
1229        });
1230        outer_record.add_field(LnmpField {
1231            fid: 10,
1232            value: LnmpValue::String("test".to_string()),
1233        });
1234
1235        let canonical = canonicalize_record(&outer_record);
1236        let fields = canonical.fields();
1237
1238        // Outer fields sorted
1239        assert_eq!(fields[0].fid, 10);
1240        assert_eq!(fields[1].fid, 50);
1241        assert_eq!(fields[2].fid, 100);
1242
1243        // Nested array fields sorted
1244        if let LnmpValue::NestedArray(arr) = &fields[1].value {
1245            let arr_fields = arr[0].fields();
1246            assert_eq!(arr_fields[0].fid, 20);
1247            assert_eq!(arr_fields[1].fid, 25);
1248        } else {
1249            panic!("Expected nested array");
1250        }
1251
1252        // Nested record fields sorted
1253        if let LnmpValue::NestedRecord(nested) = &fields[2].value {
1254            let nested_fields = nested.fields();
1255            assert_eq!(nested_fields[0].fid, 5);
1256            assert_eq!(nested_fields[1].fid, 15);
1257        } else {
1258            panic!("Expected nested record");
1259        }
1260    }
1261
1262    // Nested structure encoding tests
1263    #[test]
1264    fn test_encode_simple_nested_record() {
1265        let mut inner = LnmpRecord::new();
1266        inner.add_field(LnmpField {
1267            fid: 12,
1268            value: LnmpValue::Int(1),
1269        });
1270        inner.add_field(LnmpField {
1271            fid: 7,
1272            value: LnmpValue::Bool(true),
1273        });
1274
1275        let mut record = LnmpRecord::new();
1276        record.add_field(LnmpField {
1277            fid: 50,
1278            value: LnmpValue::NestedRecord(Box::new(inner)),
1279        });
1280
1281        let encoder = Encoder::new();
1282        let output = encoder.encode(&record);
1283
1284        // Fields should be sorted within nested record
1285        assert_eq!(output, "F50={F7=1;F12=1}");
1286    }
1287
1288    #[test]
1289    fn test_encode_empty_nested_record() {
1290        // Empty nested records are omitted during canonicalization (Requirement 9.3)
1291        let mut record = LnmpRecord::new();
1292        record.add_field(LnmpField {
1293            fid: 50,
1294            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())),
1295        });
1296
1297        let encoder = Encoder::new();
1298        let output = encoder.encode(&record);
1299        assert_eq!(output, ""); // Empty field is omitted
1300    }
1301
1302    #[test]
1303    fn test_encode_nested_record_with_various_types() {
1304        let mut inner = LnmpRecord::new();
1305        inner.add_field(LnmpField {
1306            fid: 12,
1307            value: LnmpValue::Int(14532),
1308        });
1309        inner.add_field(LnmpField {
1310            fid: 7,
1311            value: LnmpValue::Bool(true),
1312        });
1313        inner.add_field(LnmpField {
1314            fid: 23,
1315            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
1316        });
1317
1318        let mut record = LnmpRecord::new();
1319        record.add_field(LnmpField {
1320            fid: 50,
1321            value: LnmpValue::NestedRecord(Box::new(inner)),
1322        });
1323
1324        let encoder = Encoder::new();
1325        let output = encoder.encode(&record);
1326
1327        // Fields sorted: F7, F12, F23
1328        assert_eq!(output, "F50={F7=1;F12=14532;F23=[admin,dev]}");
1329    }
1330
1331    #[test]
1332    fn test_encode_deeply_nested_record() {
1333        let mut level3 = LnmpRecord::new();
1334        level3.add_field(LnmpField {
1335            fid: 1,
1336            value: LnmpValue::String("deep".to_string()),
1337        });
1338
1339        let mut level2 = LnmpRecord::new();
1340        level2.add_field(LnmpField {
1341            fid: 2,
1342            value: LnmpValue::NestedRecord(Box::new(level3)),
1343        });
1344
1345        let mut level1 = LnmpRecord::new();
1346        level1.add_field(LnmpField {
1347            fid: 3,
1348            value: LnmpValue::NestedRecord(Box::new(level2)),
1349        });
1350
1351        let encoder = Encoder::new();
1352        let output = encoder.encode(&level1);
1353        assert_eq!(output, "F3={F2={F1=deep}}");
1354    }
1355
1356    #[test]
1357    fn test_encode_simple_nested_array() {
1358        let mut rec1 = LnmpRecord::new();
1359        rec1.add_field(LnmpField {
1360            fid: 12,
1361            value: LnmpValue::Int(1),
1362        });
1363
1364        let mut rec2 = LnmpRecord::new();
1365        rec2.add_field(LnmpField {
1366            fid: 12,
1367            value: LnmpValue::Int(2),
1368        });
1369
1370        let mut rec3 = LnmpRecord::new();
1371        rec3.add_field(LnmpField {
1372            fid: 12,
1373            value: LnmpValue::Int(3),
1374        });
1375
1376        let mut record = LnmpRecord::new();
1377        record.add_field(LnmpField {
1378            fid: 60,
1379            value: LnmpValue::NestedArray(vec![rec1, rec2, rec3]),
1380        });
1381
1382        let encoder = Encoder::new();
1383        let output = encoder.encode(&record);
1384        assert_eq!(output, "F60=[{F12=1},{F12=2},{F12=3}]");
1385    }
1386
1387    #[test]
1388    fn test_encode_empty_nested_array() {
1389        // Empty nested arrays are omitted during canonicalization (Requirement 9.3)
1390        let mut record = LnmpRecord::new();
1391        record.add_field(LnmpField {
1392            fid: 60,
1393            value: LnmpValue::NestedArray(vec![]),
1394        });
1395
1396        let encoder = Encoder::new();
1397        let output = encoder.encode(&record);
1398        assert_eq!(output, ""); // Empty field is omitted
1399    }
1400
1401    #[test]
1402    fn test_encode_nested_array_with_multiple_fields() {
1403        let mut rec1 = LnmpRecord::new();
1404        rec1.add_field(LnmpField {
1405            fid: 1,
1406            value: LnmpValue::String("alice".to_string()),
1407        });
1408        rec1.add_field(LnmpField {
1409            fid: 2,
1410            value: LnmpValue::String("admin".to_string()),
1411        });
1412
1413        let mut rec2 = LnmpRecord::new();
1414        rec2.add_field(LnmpField {
1415            fid: 1,
1416            value: LnmpValue::String("bob".to_string()),
1417        });
1418        rec2.add_field(LnmpField {
1419            fid: 2,
1420            value: LnmpValue::String("user".to_string()),
1421        });
1422
1423        let mut record = LnmpRecord::new();
1424        record.add_field(LnmpField {
1425            fid: 200,
1426            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1427        });
1428
1429        let encoder = Encoder::new();
1430        let output = encoder.encode(&record);
1431        assert_eq!(output, "F200=[{F1=alice;F2=admin},{F1=bob;F2=user}]");
1432    }
1433
1434    #[test]
1435    fn test_encode_nested_array_preserves_order() {
1436        // Requirement 5.3: Element order must be preserved
1437        let mut rec1 = LnmpRecord::new();
1438        rec1.add_field(LnmpField {
1439            fid: 1,
1440            value: LnmpValue::String("first".to_string()),
1441        });
1442
1443        let mut rec2 = LnmpRecord::new();
1444        rec2.add_field(LnmpField {
1445            fid: 1,
1446            value: LnmpValue::String("second".to_string()),
1447        });
1448
1449        let mut rec3 = LnmpRecord::new();
1450        rec3.add_field(LnmpField {
1451            fid: 1,
1452            value: LnmpValue::String("third".to_string()),
1453        });
1454
1455        let mut record = LnmpRecord::new();
1456        record.add_field(LnmpField {
1457            fid: 60,
1458            value: LnmpValue::NestedArray(vec![rec1, rec2, rec3]),
1459        });
1460
1461        let encoder = Encoder::new();
1462        let output = encoder.encode(&record);
1463        assert_eq!(output, "F60=[{F1=first},{F1=second},{F1=third}]");
1464    }
1465
1466    #[test]
1467    fn test_encode_nested_array_fields_sorted() {
1468        // Fields within each nested record should be sorted
1469        let mut rec1 = LnmpRecord::new();
1470        rec1.add_field(LnmpField {
1471            fid: 20,
1472            value: LnmpValue::Int(2),
1473        });
1474        rec1.add_field(LnmpField {
1475            fid: 10,
1476            value: LnmpValue::Int(1),
1477        });
1478
1479        let mut record = LnmpRecord::new();
1480        record.add_field(LnmpField {
1481            fid: 60,
1482            value: LnmpValue::NestedArray(vec![rec1]),
1483        });
1484
1485        let encoder = Encoder::new();
1486        let output = encoder.encode(&record);
1487        // Fields should be sorted: F10, F20
1488        assert_eq!(output, "F60=[{F10=1;F20=2}]");
1489    }
1490
1491    #[test]
1492    fn test_encode_mixed_nested_structures() {
1493        // Test record with both nested record and nested array
1494        let mut inner_record = LnmpRecord::new();
1495        inner_record.add_field(LnmpField {
1496            fid: 1,
1497            value: LnmpValue::String("nested".to_string()),
1498        });
1499
1500        let mut array_rec = LnmpRecord::new();
1501        array_rec.add_field(LnmpField {
1502            fid: 2,
1503            value: LnmpValue::Int(42),
1504        });
1505
1506        let mut record = LnmpRecord::new();
1507        record.add_field(LnmpField {
1508            fid: 50,
1509            value: LnmpValue::NestedRecord(Box::new(inner_record)),
1510        });
1511        record.add_field(LnmpField {
1512            fid: 60,
1513            value: LnmpValue::NestedArray(vec![array_rec]),
1514        });
1515        record.add_field(LnmpField {
1516            fid: 10,
1517            value: LnmpValue::String("top".to_string()),
1518        });
1519
1520        let encoder = Encoder::new();
1521        let output = encoder.encode(&record);
1522        // Fields sorted: F10, F50, F60
1523        assert_eq!(output, "F10=top\nF50={F1=nested}\nF60=[{F2=42}]");
1524    }
1525
1526    #[test]
1527    fn test_encode_nested_record_with_type_hints() {
1528        use crate::config::EncoderConfig;
1529
1530        let mut inner = LnmpRecord::new();
1531        inner.add_field(LnmpField {
1532            fid: 12,
1533            value: LnmpValue::Int(14532),
1534        });
1535        inner.add_field(LnmpField {
1536            fid: 7,
1537            value: LnmpValue::Bool(true),
1538        });
1539
1540        let mut record = LnmpRecord::new();
1541        record.add_field(LnmpField {
1542            fid: 50,
1543            value: LnmpValue::NestedRecord(Box::new(inner)),
1544        });
1545
1546        let config = EncoderConfig::new().with_type_hints(true);
1547
1548        let encoder = Encoder::with_config(config);
1549        let output = encoder.encode(&record);
1550
1551        // Type hints should be included
1552        assert_eq!(output, "F50:r={F7:b=1;F12:i=14532}");
1553    }
1554
1555    #[test]
1556    fn test_encode_nested_array_with_type_hints() {
1557        use crate::config::EncoderConfig;
1558
1559        let mut rec1 = LnmpRecord::new();
1560        rec1.add_field(LnmpField {
1561            fid: 12,
1562            value: LnmpValue::Int(1),
1563        });
1564
1565        let mut rec2 = LnmpRecord::new();
1566        rec2.add_field(LnmpField {
1567            fid: 12,
1568            value: LnmpValue::Int(2),
1569        });
1570
1571        let mut record = LnmpRecord::new();
1572        record.add_field(LnmpField {
1573            fid: 60,
1574            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1575        });
1576
1577        let config = EncoderConfig::new().with_type_hints(true);
1578
1579        let encoder = Encoder::with_config(config);
1580        let output = encoder.encode(&record);
1581
1582        // Type hints should be included
1583        assert_eq!(output, "F60:ra=[{F12:i=1},{F12:i=2}]");
1584    }
1585
1586    #[test]
1587    fn test_round_trip_nested_record() {
1588        use crate::parser::Parser;
1589
1590        let mut inner = LnmpRecord::new();
1591        inner.add_field(LnmpField {
1592            fid: 12,
1593            value: LnmpValue::Int(14532),
1594        });
1595        inner.add_field(LnmpField {
1596            fid: 7,
1597            value: LnmpValue::Bool(true),
1598        });
1599
1600        let mut record = LnmpRecord::new();
1601        record.add_field(LnmpField {
1602            fid: 50,
1603            value: LnmpValue::NestedRecord(Box::new(inner)),
1604        });
1605
1606        let encoder = Encoder::new();
1607        let output = encoder.encode(&record);
1608
1609        let mut parser = Parser::new(&output).unwrap();
1610        let parsed = parser.parse_record().unwrap();
1611
1612        // Compare canonical versions since encoder sorts fields
1613        let canonical_original = canonicalize_record(&record);
1614        assert_eq!(canonical_original, parsed);
1615    }
1616
1617    #[test]
1618    fn test_round_trip_nested_array() {
1619        use crate::parser::Parser;
1620
1621        let mut rec1 = LnmpRecord::new();
1622        rec1.add_field(LnmpField {
1623            fid: 1,
1624            value: LnmpValue::String("alice".to_string()),
1625        });
1626
1627        let mut rec2 = LnmpRecord::new();
1628        rec2.add_field(LnmpField {
1629            fid: 1,
1630            value: LnmpValue::String("bob".to_string()),
1631        });
1632
1633        let mut record = LnmpRecord::new();
1634        record.add_field(LnmpField {
1635            fid: 200,
1636            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1637        });
1638
1639        let encoder = Encoder::new();
1640        let output = encoder.encode(&record);
1641
1642        let mut parser = Parser::new(&output).unwrap();
1643        let parsed = parser.parse_record().unwrap();
1644
1645        assert_eq!(record, parsed);
1646    }
1647
1648    #[test]
1649    fn test_round_trip_complex_nested_structure() {
1650        use crate::parser::Parser;
1651
1652        // Create a complex structure with multiple levels of nesting
1653        let mut level3 = LnmpRecord::new();
1654        level3.add_field(LnmpField {
1655            fid: 1,
1656            value: LnmpValue::Int(42),
1657        });
1658
1659        let mut level2 = LnmpRecord::new();
1660        level2.add_field(LnmpField {
1661            fid: 10,
1662            value: LnmpValue::String("nested".to_string()),
1663        });
1664        level2.add_field(LnmpField {
1665            fid: 11,
1666            value: LnmpValue::NestedRecord(Box::new(level3)),
1667        });
1668
1669        let mut array_rec = LnmpRecord::new();
1670        array_rec.add_field(LnmpField {
1671            fid: 5,
1672            value: LnmpValue::Bool(true),
1673        });
1674
1675        let mut record = LnmpRecord::new();
1676        record.add_field(LnmpField {
1677            fid: 100,
1678            value: LnmpValue::NestedRecord(Box::new(level2)),
1679        });
1680        record.add_field(LnmpField {
1681            fid: 200,
1682            value: LnmpValue::NestedArray(vec![array_rec]),
1683        });
1684
1685        let encoder = Encoder::new();
1686        let output = encoder.encode(&record);
1687
1688        let mut parser = Parser::new(&output).unwrap();
1689        let parsed = parser.parse_record().unwrap();
1690
1691        assert_eq!(record, parsed);
1692    }
1693
1694    // Checksum encoding tests
1695    #[test]
1696    fn test_encode_with_checksum() {
1697        use crate::config::EncoderConfig;
1698
1699        let mut record = LnmpRecord::new();
1700        record.add_field(LnmpField {
1701            fid: 12,
1702            value: LnmpValue::Int(14532),
1703        });
1704
1705        let config = EncoderConfig::new().with_checksums(true);
1706
1707        let encoder = Encoder::with_config(config);
1708        let output = encoder.encode(&record);
1709
1710        // Should include checksum
1711        assert!(output.contains('#'));
1712        assert!(output.starts_with("F12=14532#"));
1713
1714        // Checksum should be 8 hex characters
1715        let parts: Vec<&str> = output.split('#').collect();
1716        assert_eq!(parts.len(), 2);
1717        assert_eq!(parts[1].len(), 8);
1718    }
1719
1720    #[test]
1721    fn test_encode_with_checksum_and_type_hints() {
1722        use crate::config::EncoderConfig;
1723
1724        let mut record = LnmpRecord::new();
1725        record.add_field(LnmpField {
1726            fid: 12,
1727            value: LnmpValue::Int(14532),
1728        });
1729
1730        let config = EncoderConfig::new()
1731            .with_type_hints(true)
1732            .with_checksums(true);
1733
1734        let encoder = Encoder::with_config(config);
1735        let output = encoder.encode(&record);
1736
1737        // Should include both type hint and checksum
1738        assert!(output.contains(':'));
1739        assert!(output.contains('#'));
1740        assert!(output.starts_with("F12:i=14532#"));
1741    }
1742
1743    #[test]
1744    fn test_encode_without_checksum() {
1745        use crate::config::EncoderConfig;
1746
1747        let mut record = LnmpRecord::new();
1748        record.add_field(LnmpField {
1749            fid: 12,
1750            value: LnmpValue::Int(14532),
1751        });
1752
1753        let config = EncoderConfig::new();
1754
1755        let encoder = Encoder::with_config(config);
1756        let output = encoder.encode(&record);
1757
1758        // Should not include checksum
1759        assert!(!output.contains('#'));
1760        assert_eq!(output, "F12=14532");
1761    }
1762
1763    #[test]
1764    fn test_encode_multiple_fields_with_checksums() {
1765        use crate::config::EncoderConfig;
1766
1767        let mut record = LnmpRecord::new();
1768        record.add_field(LnmpField {
1769            fid: 12,
1770            value: LnmpValue::Int(14532),
1771        });
1772        record.add_field(LnmpField {
1773            fid: 7,
1774            value: LnmpValue::Bool(true),
1775        });
1776
1777        let config = EncoderConfig::new()
1778            .with_type_hints(true)
1779            .with_checksums(true);
1780
1781        let encoder = Encoder::with_config(config);
1782        let output = encoder.encode(&record);
1783
1784        // Each field should have a checksum
1785        let lines: Vec<&str> = output.lines().collect();
1786        assert_eq!(lines.len(), 2);
1787        assert!(lines[0].contains('#'));
1788        assert!(lines[1].contains('#'));
1789    }
1790
1791    #[test]
1792    fn test_checksum_deterministic() {
1793        use crate::config::EncoderConfig;
1794
1795        let mut record = LnmpRecord::new();
1796        record.add_field(LnmpField {
1797            fid: 12,
1798            value: LnmpValue::Int(14532),
1799        });
1800
1801        let config = EncoderConfig::new().with_type_hints(true);
1802
1803        let encoder = Encoder::with_config(config);
1804
1805        // Encode multiple times
1806        let output1 = encoder.encode(&record);
1807        let output2 = encoder.encode(&record);
1808        let output3 = encoder.encode(&record);
1809
1810        // All outputs should be identical
1811        assert_eq!(output1, output2);
1812        assert_eq!(output2, output3);
1813    }
1814
1815    #[test]
1816    fn test_encode_nested_record_with_checksum() {
1817        use crate::config::EncoderConfig;
1818
1819        let mut inner = LnmpRecord::new();
1820        inner.add_field(LnmpField {
1821            fid: 12,
1822            value: LnmpValue::Int(1),
1823        });
1824
1825        let mut record = LnmpRecord::new();
1826        record.add_field(LnmpField {
1827            fid: 50,
1828            value: LnmpValue::NestedRecord(Box::new(inner)),
1829        });
1830
1831        let config = EncoderConfig::new()
1832            .with_type_hints(true)
1833            .with_checksums(true);
1834
1835        let encoder = Encoder::with_config(config);
1836        let output = encoder.encode(&record);
1837
1838        // Should include checksum for the nested record field
1839        assert!(output.contains('#'));
1840        // The nested record value includes type hints for inner fields
1841        assert!(output.starts_with("F50:r={F12:i=1}#"));
1842
1843        // Verify checksum is 8 hex characters
1844        let parts: Vec<&str> = output.split('#').collect();
1845        assert_eq!(parts.len(), 2);
1846        assert_eq!(parts[1].len(), 8);
1847    }
1848
1849    #[test]
1850    fn test_encode_nested_array_with_checksum() {
1851        use crate::config::EncoderConfig;
1852
1853        let mut rec1 = LnmpRecord::new();
1854        rec1.add_field(LnmpField {
1855            fid: 12,
1856            value: LnmpValue::Int(1),
1857        });
1858
1859        let mut record = LnmpRecord::new();
1860        record.add_field(LnmpField {
1861            fid: 60,
1862            value: LnmpValue::NestedArray(vec![rec1]),
1863        });
1864
1865        let config = EncoderConfig::new()
1866            .with_type_hints(true)
1867            .with_checksums(true);
1868
1869        let encoder = Encoder::with_config(config);
1870        let output = encoder.encode(&record);
1871
1872        // Should include checksum for the nested array field
1873        assert!(output.contains('#'));
1874        assert!(output.starts_with("F60:ra=[{F12:i=1}]#"));
1875
1876        // Verify checksum is 8 hex characters
1877        let parts: Vec<&str> = output.split('#').collect();
1878        assert_eq!(parts.len(), 2);
1879        assert_eq!(parts[1].len(), 8);
1880    }
1881
1882    #[test]
1883    fn test_checksum_different_for_different_values() {
1884        use crate::config::EncoderConfig;
1885
1886        let mut record1 = LnmpRecord::new();
1887        record1.add_field(LnmpField {
1888            fid: 12,
1889            value: LnmpValue::Int(14532),
1890        });
1891
1892        let mut record2 = LnmpRecord::new();
1893        record2.add_field(LnmpField {
1894            fid: 12,
1895            value: LnmpValue::Int(14533),
1896        });
1897
1898        let config = EncoderConfig::new()
1899            .with_type_hints(true)
1900            .with_checksums(true);
1901
1902        let encoder = Encoder::with_config(config);
1903
1904        let output1 = encoder.encode(&record1);
1905        let output2 = encoder.encode(&record2);
1906
1907        // Checksums should be different
1908        assert_ne!(output1, output2);
1909
1910        // Extract checksums
1911        let checksum1 = output1.split('#').nth(1).unwrap();
1912        let checksum2 = output2.split('#').nth(1).unwrap();
1913        assert_ne!(checksum1, checksum2);
1914    }
1915
1916    // Canonicalization tests for v0.5 requirements
1917
1918    #[test]
1919    fn test_canonicalize_field_ordering_multiple_levels() {
1920        // Test field ordering at multiple nesting levels (Requirement 9.1)
1921        let mut level3 = LnmpRecord::new();
1922        level3.add_field(LnmpField {
1923            fid: 30,
1924            value: LnmpValue::Int(3),
1925        });
1926        level3.add_field(LnmpField {
1927            fid: 10,
1928            value: LnmpValue::Int(1),
1929        });
1930        level3.add_field(LnmpField {
1931            fid: 20,
1932            value: LnmpValue::Int(2),
1933        });
1934
1935        let mut level2 = LnmpRecord::new();
1936        level2.add_field(LnmpField {
1937            fid: 300,
1938            value: LnmpValue::NestedRecord(Box::new(level3)),
1939        });
1940        level2.add_field(LnmpField {
1941            fid: 100,
1942            value: LnmpValue::Int(10),
1943        });
1944        level2.add_field(LnmpField {
1945            fid: 200,
1946            value: LnmpValue::Int(20),
1947        });
1948
1949        let mut level1 = LnmpRecord::new();
1950        level1.add_field(LnmpField {
1951            fid: 3000,
1952            value: LnmpValue::NestedRecord(Box::new(level2)),
1953        });
1954        level1.add_field(LnmpField {
1955            fid: 1000,
1956            value: LnmpValue::Int(100),
1957        });
1958        level1.add_field(LnmpField {
1959            fid: 2000,
1960            value: LnmpValue::Int(200),
1961        });
1962
1963        let canonical = canonicalize_record(&level1);
1964        let fields = canonical.fields();
1965
1966        // Level 1 fields should be sorted
1967        assert_eq!(fields[0].fid, 1000);
1968        assert_eq!(fields[1].fid, 2000);
1969        assert_eq!(fields[2].fid, 3000);
1970
1971        // Level 2 fields should be sorted
1972        if let LnmpValue::NestedRecord(level2_rec) = &fields[2].value {
1973            let level2_fields = level2_rec.fields();
1974            assert_eq!(level2_fields[0].fid, 100);
1975            assert_eq!(level2_fields[1].fid, 200);
1976            assert_eq!(level2_fields[2].fid, 300);
1977
1978            // Level 3 fields should be sorted
1979            if let LnmpValue::NestedRecord(level3_rec) = &level2_fields[2].value {
1980                let level3_fields = level3_rec.fields();
1981                assert_eq!(level3_fields[0].fid, 10);
1982                assert_eq!(level3_fields[1].fid, 20);
1983                assert_eq!(level3_fields[2].fid, 30);
1984            } else {
1985                panic!("Expected level 3 nested record");
1986            }
1987        } else {
1988            panic!("Expected level 2 nested record");
1989        }
1990    }
1991
1992    #[test]
1993    fn test_canonicalize_array_record_ordering() {
1994        // Test that records within nested arrays have sorted fields (Requirement 9.2)
1995        let mut rec1 = LnmpRecord::new();
1996        rec1.add_field(LnmpField {
1997            fid: 50,
1998            value: LnmpValue::Int(5),
1999        });
2000        rec1.add_field(LnmpField {
2001            fid: 10,
2002            value: LnmpValue::Int(1),
2003        });
2004        rec1.add_field(LnmpField {
2005            fid: 30,
2006            value: LnmpValue::Int(3),
2007        });
2008
2009        let mut rec2 = LnmpRecord::new();
2010        rec2.add_field(LnmpField {
2011            fid: 80,
2012            value: LnmpValue::Int(8),
2013        });
2014        rec2.add_field(LnmpField {
2015            fid: 20,
2016            value: LnmpValue::Int(2),
2017        });
2018        rec2.add_field(LnmpField {
2019            fid: 60,
2020            value: LnmpValue::Int(6),
2021        });
2022
2023        let mut outer = LnmpRecord::new();
2024        outer.add_field(LnmpField {
2025            fid: 100,
2026            value: LnmpValue::NestedArray(vec![rec1, rec2]),
2027        });
2028
2029        let canonical = canonicalize_record(&outer);
2030        let fields = canonical.fields();
2031
2032        assert_eq!(fields.len(), 1);
2033        assert_eq!(fields[0].fid, 100);
2034
2035        // Each record in the array should have sorted fields
2036        if let LnmpValue::NestedArray(arr) = &fields[0].value {
2037            assert_eq!(arr.len(), 2);
2038
2039            // First record fields should be sorted
2040            let rec1_fields = arr[0].fields();
2041            assert_eq!(rec1_fields[0].fid, 10);
2042            assert_eq!(rec1_fields[1].fid, 30);
2043            assert_eq!(rec1_fields[2].fid, 50);
2044
2045            // Second record fields should be sorted
2046            let rec2_fields = arr[1].fields();
2047            assert_eq!(rec2_fields[0].fid, 20);
2048            assert_eq!(rec2_fields[1].fid, 60);
2049            assert_eq!(rec2_fields[2].fid, 80);
2050        } else {
2051            panic!("Expected nested array");
2052        }
2053    }
2054
2055    #[test]
2056    fn test_canonicalize_empty_field_omission() {
2057        // Test that empty fields are omitted (Requirement 9.3)
2058        let mut record = LnmpRecord::new();
2059        record.add_field(LnmpField {
2060            fid: 10,
2061            value: LnmpValue::Int(42),
2062        });
2063        record.add_field(LnmpField {
2064            fid: 20,
2065            value: LnmpValue::String("".to_string()), // Empty string
2066        });
2067        record.add_field(LnmpField {
2068            fid: 30,
2069            value: LnmpValue::StringArray(vec![]), // Empty array
2070        });
2071        record.add_field(LnmpField {
2072            fid: 40,
2073            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())), // Empty nested record
2074        });
2075        record.add_field(LnmpField {
2076            fid: 50,
2077            value: LnmpValue::NestedArray(vec![]), // Empty nested array
2078        });
2079        record.add_field(LnmpField {
2080            fid: 60,
2081            value: LnmpValue::String("not_empty".to_string()),
2082        });
2083
2084        let canonical = canonicalize_record(&record);
2085        let fields = canonical.fields();
2086
2087        // Only non-empty fields should remain
2088        assert_eq!(fields.len(), 2);
2089        assert_eq!(fields[0].fid, 10);
2090        assert_eq!(fields[0].value, LnmpValue::Int(42));
2091        assert_eq!(fields[1].fid, 60);
2092        assert_eq!(fields[1].value, LnmpValue::String("not_empty".to_string()));
2093    }
2094
2095    #[test]
2096    fn test_canonicalize_empty_field_omission_nested() {
2097        // Test that empty fields are omitted in nested structures
2098        let mut inner = LnmpRecord::new();
2099        inner.add_field(LnmpField {
2100            fid: 1,
2101            value: LnmpValue::Int(42),
2102        });
2103        inner.add_field(LnmpField {
2104            fid: 2,
2105            value: LnmpValue::String("".to_string()), // Empty string
2106        });
2107        inner.add_field(LnmpField {
2108            fid: 3,
2109            value: LnmpValue::String("value".to_string()),
2110        });
2111
2112        let mut outer = LnmpRecord::new();
2113        outer.add_field(LnmpField {
2114            fid: 100,
2115            value: LnmpValue::NestedRecord(Box::new(inner)),
2116        });
2117        outer.add_field(LnmpField {
2118            fid: 200,
2119            value: LnmpValue::StringArray(vec![]), // Empty array
2120        });
2121
2122        let canonical = canonicalize_record(&outer);
2123        let fields = canonical.fields();
2124
2125        // Only the nested record should remain (empty array omitted)
2126        assert_eq!(fields.len(), 1);
2127        assert_eq!(fields[0].fid, 100);
2128
2129        // Check nested record has empty field omitted
2130        if let LnmpValue::NestedRecord(nested) = &fields[0].value {
2131            let nested_fields = nested.fields();
2132            assert_eq!(nested_fields.len(), 2); // Only F1 and F3, F2 omitted
2133            assert_eq!(nested_fields[0].fid, 1);
2134            assert_eq!(nested_fields[1].fid, 3);
2135        } else {
2136            panic!("Expected nested record");
2137        }
2138    }
2139
2140    #[test]
2141    fn test_canonicalize_round_trip_stability_simple() {
2142        // Test round-trip stability for simple record (Requirement 9.4)
2143        let mut record = LnmpRecord::new();
2144        record.add_field(LnmpField {
2145            fid: 100,
2146            value: LnmpValue::Int(3),
2147        });
2148        record.add_field(LnmpField {
2149            fid: 5,
2150            value: LnmpValue::Int(1),
2151        });
2152        record.add_field(LnmpField {
2153            fid: 50,
2154            value: LnmpValue::Int(2),
2155        });
2156
2157        assert!(validate_round_trip_stability(&record));
2158    }
2159
2160    #[test]
2161    fn test_canonicalize_round_trip_stability_nested() {
2162        // Test round-trip stability for nested structures
2163        let mut inner = LnmpRecord::new();
2164        inner.add_field(LnmpField {
2165            fid: 30,
2166            value: LnmpValue::Int(3),
2167        });
2168        inner.add_field(LnmpField {
2169            fid: 10,
2170            value: LnmpValue::Int(1),
2171        });
2172        inner.add_field(LnmpField {
2173            fid: 20,
2174            value: LnmpValue::Int(2),
2175        });
2176
2177        let mut outer = LnmpRecord::new();
2178        outer.add_field(LnmpField {
2179            fid: 300,
2180            value: LnmpValue::NestedRecord(Box::new(inner)),
2181        });
2182        outer.add_field(LnmpField {
2183            fid: 100,
2184            value: LnmpValue::Int(10),
2185        });
2186        outer.add_field(LnmpField {
2187            fid: 200,
2188            value: LnmpValue::Int(20),
2189        });
2190
2191        assert!(validate_round_trip_stability(&outer));
2192    }
2193
2194    #[test]
2195    fn test_canonicalize_round_trip_stability_deeply_nested() {
2196        // Test round-trip stability with deeply nested structures (depth 5)
2197        let mut level5 = LnmpRecord::new();
2198        level5.add_field(LnmpField {
2199            fid: 5,
2200            value: LnmpValue::Int(5),
2201        });
2202        level5.add_field(LnmpField {
2203            fid: 1,
2204            value: LnmpValue::Int(1),
2205        });
2206
2207        let mut level4 = LnmpRecord::new();
2208        level4.add_field(LnmpField {
2209            fid: 4,
2210            value: LnmpValue::NestedRecord(Box::new(level5)),
2211        });
2212
2213        let mut level3 = LnmpRecord::new();
2214        level3.add_field(LnmpField {
2215            fid: 3,
2216            value: LnmpValue::NestedRecord(Box::new(level4)),
2217        });
2218
2219        let mut level2 = LnmpRecord::new();
2220        level2.add_field(LnmpField {
2221            fid: 2,
2222            value: LnmpValue::NestedRecord(Box::new(level3)),
2223        });
2224
2225        let mut level1 = LnmpRecord::new();
2226        level1.add_field(LnmpField {
2227            fid: 1,
2228            value: LnmpValue::NestedRecord(Box::new(level2)),
2229        });
2230
2231        assert!(validate_round_trip_stability(&level1));
2232    }
2233
2234    #[test]
2235    fn test_canonicalize_round_trip_stability_with_arrays() {
2236        // Test round-trip stability with nested arrays
2237        let mut rec1 = LnmpRecord::new();
2238        rec1.add_field(LnmpField {
2239            fid: 50,
2240            value: LnmpValue::Int(5),
2241        });
2242        rec1.add_field(LnmpField {
2243            fid: 10,
2244            value: LnmpValue::Int(1),
2245        });
2246
2247        let mut rec2 = LnmpRecord::new();
2248        rec2.add_field(LnmpField {
2249            fid: 80,
2250            value: LnmpValue::Int(8),
2251        });
2252        rec2.add_field(LnmpField {
2253            fid: 20,
2254            value: LnmpValue::Int(2),
2255        });
2256
2257        let mut outer = LnmpRecord::new();
2258        outer.add_field(LnmpField {
2259            fid: 100,
2260            value: LnmpValue::NestedArray(vec![rec1, rec2]),
2261        });
2262        outer.add_field(LnmpField {
2263            fid: 50,
2264            value: LnmpValue::String("test".to_string()),
2265        });
2266
2267        assert!(validate_round_trip_stability(&outer));
2268    }
2269
2270    #[test]
2271    fn test_canonicalize_round_trip_stability_with_empty_fields() {
2272        // Test round-trip stability with empty fields that should be omitted
2273        let mut record = LnmpRecord::new();
2274        record.add_field(LnmpField {
2275            fid: 10,
2276            value: LnmpValue::Int(42),
2277        });
2278        record.add_field(LnmpField {
2279            fid: 20,
2280            value: LnmpValue::String("".to_string()), // Empty string
2281        });
2282        record.add_field(LnmpField {
2283            fid: 30,
2284            value: LnmpValue::StringArray(vec![]), // Empty array
2285        });
2286
2287        assert!(validate_round_trip_stability(&record));
2288    }
2289
2290    #[test]
2291    fn test_canonicalize_round_trip_stability_mixed_structures() {
2292        // Test round-trip stability with mixed nested structures
2293        let mut inner_record = LnmpRecord::new();
2294        inner_record.add_field(LnmpField {
2295            fid: 15,
2296            value: LnmpValue::Int(5),
2297        });
2298        inner_record.add_field(LnmpField {
2299            fid: 5,
2300            value: LnmpValue::Int(3),
2301        });
2302
2303        let mut array_record = LnmpRecord::new();
2304        array_record.add_field(LnmpField {
2305            fid: 25,
2306            value: LnmpValue::Int(7),
2307        });
2308        array_record.add_field(LnmpField {
2309            fid: 20,
2310            value: LnmpValue::Int(6),
2311        });
2312
2313        let mut outer = LnmpRecord::new();
2314        outer.add_field(LnmpField {
2315            fid: 100,
2316            value: LnmpValue::NestedRecord(Box::new(inner_record)),
2317        });
2318        outer.add_field(LnmpField {
2319            fid: 50,
2320            value: LnmpValue::NestedArray(vec![array_record]),
2321        });
2322        outer.add_field(LnmpField {
2323            fid: 10,
2324            value: LnmpValue::String("test".to_string()),
2325        });
2326
2327        assert!(validate_round_trip_stability(&outer));
2328    }
2329
2330    #[test]
2331    fn test_is_empty_value() {
2332        // Test the is_empty_value helper function
2333        assert!(is_empty_value(&LnmpValue::String("".to_string())));
2334        assert!(!is_empty_value(&LnmpValue::String("not_empty".to_string())));
2335
2336        assert!(is_empty_value(&LnmpValue::StringArray(vec![])));
2337        assert!(!is_empty_value(&LnmpValue::StringArray(vec![
2338            "item".to_string()
2339        ])));
2340
2341        assert!(is_empty_value(&LnmpValue::NestedRecord(Box::new(
2342            LnmpRecord::new()
2343        ))));
2344        let mut non_empty_record = LnmpRecord::new();
2345        non_empty_record.add_field(LnmpField {
2346            fid: 1,
2347            value: LnmpValue::Int(42),
2348        });
2349        assert!(!is_empty_value(&LnmpValue::NestedRecord(Box::new(
2350            non_empty_record
2351        ))));
2352
2353        assert!(is_empty_value(&LnmpValue::NestedArray(vec![])));
2354        let mut rec = LnmpRecord::new();
2355        rec.add_field(LnmpField {
2356            fid: 1,
2357            value: LnmpValue::Int(42),
2358        });
2359        assert!(!is_empty_value(&LnmpValue::NestedArray(vec![rec])));
2360
2361        // Primitive values are never empty
2362        assert!(!is_empty_value(&LnmpValue::Int(0)));
2363        assert!(!is_empty_value(&LnmpValue::Int(42)));
2364        assert!(!is_empty_value(&LnmpValue::Float(0.0)));
2365        assert!(!is_empty_value(&LnmpValue::Float(3.14)));
2366        assert!(!is_empty_value(&LnmpValue::Bool(true)));
2367        assert!(!is_empty_value(&LnmpValue::Bool(false)));
2368    }
2369}