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