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