lnmp_codec/
parser.rs

1//! Parser for converting LNMP text format into structured records.
2
3use std::borrow::Cow;
4
5use crate::config::{ParserConfig, ParsingMode, TextInputMode};
6use crate::error::LnmpError;
7use crate::lexer::{Lexer, Token};
8use crate::normalizer::ValueNormalizer;
9use lnmp_core::checksum::SemanticChecksum;
10use lnmp_core::{FieldId, LnmpField, LnmpRecord, LnmpValue, TypeHint};
11use lnmp_sanitize::{sanitize_lnmp_text, SanitizationConfig};
12
13/// Parser for LNMP text format
14pub struct Parser<'a> {
15    lexer: Lexer<'a>,
16    current_token: Token,
17    config: ParserConfig,
18    // current nesting depth for nested records/arrays
19    nesting_depth: usize,
20    normalizer: Option<ValueNormalizer>,
21}
22
23impl<'a> Parser<'a> {
24    /// Creates a new parser for the given input (defaults to loose mode)
25    pub fn new(input: &'a str) -> Result<Self, LnmpError> {
26        Self::with_config(input, ParserConfig::default())
27    }
28
29    /// Creates a new parser using strict text input handling.
30    pub fn new_strict(input: &'a str) -> Result<Self, LnmpError> {
31        Self::with_config(
32            input,
33            ParserConfig {
34                text_input_mode: TextInputMode::Strict,
35                ..ParserConfig::default()
36            },
37        )
38    }
39
40    /// Creates a new parser using lenient text sanitization before parsing.
41    pub fn new_lenient(input: &'a str) -> Result<Self, LnmpError> {
42        Self::with_config(
43            input,
44            ParserConfig {
45                text_input_mode: TextInputMode::Lenient,
46                ..ParserConfig::default()
47            },
48        )
49    }
50
51    /// Creates a new parser with specified parsing mode
52    pub fn with_mode(input: &'a str, mode: ParsingMode) -> Result<Self, LnmpError> {
53        let config = ParserConfig {
54            mode,
55            ..Default::default()
56        };
57        Self::with_config(input, config)
58    }
59
60    /// Creates a new parser with specified configuration
61    pub fn with_config(input: &'a str, config: ParserConfig) -> Result<Self, LnmpError> {
62        let input_cow = match config.text_input_mode {
63            TextInputMode::Strict => Cow::Borrowed(input),
64            TextInputMode::Lenient => sanitize_lnmp_text(input, &SanitizationConfig::default()),
65        };
66
67        if config.mode == ParsingMode::Strict {
68            Self::check_for_comments(input_cow.as_ref())?;
69        }
70
71        let mut lexer = match input_cow {
72            Cow::Borrowed(s) => Lexer::new(s),
73            Cow::Owned(s) => {
74                let span_map = crate::lexer::build_span_map(s.as_str(), input);
75                Lexer::new_owned_with_original(s, input.to_string(), span_map)
76            }
77        };
78        let current_token = lexer.next_token()?;
79
80        let normalizer = config.semantic_dictionary.as_ref().map(|dict| {
81            ValueNormalizer::new(crate::normalizer::NormalizationConfig {
82                semantic_dictionary: Some(dict.clone()),
83                ..crate::normalizer::NormalizationConfig::default()
84            })
85        });
86
87        Ok(Self {
88            lexer,
89            current_token,
90            config,
91            nesting_depth: 0,
92            normalizer,
93        })
94    }
95
96    /// Returns the current parsing mode
97    pub fn mode(&self) -> ParsingMode {
98        self.config.mode
99    }
100
101    /// Advances to the next token
102    fn advance(&mut self) -> Result<(), LnmpError> {
103        self.current_token = self.lexer.next_token()?;
104        Ok(())
105    }
106
107    /// Expects a specific token and advances
108    fn expect(&mut self, expected: Token) -> Result<(), LnmpError> {
109        if self.current_token == expected {
110            self.advance()
111        } else {
112            let (line, column) = self.lexer.position_original();
113            Err(LnmpError::UnexpectedToken {
114                expected: format!("{:?}", expected),
115                found: self.current_token.clone(),
116                line,
117                column,
118            })
119        }
120    }
121
122    /// Skips newlines
123    fn skip_newlines(&mut self) -> Result<(), LnmpError> {
124        while self.current_token == Token::Newline {
125            self.advance()?;
126        }
127        Ok(())
128    }
129
130    /// Skips a comment (Hash token followed by text until newline)
131    fn skip_comment(&mut self) -> Result<(), LnmpError> {
132        // Consume the Hash token
133        self.advance()?;
134
135        // Skip until newline or EOF
136        while self.current_token != Token::Newline && self.current_token != Token::Eof {
137            self.advance()?;
138        }
139
140        Ok(())
141    }
142
143    /// Checks if the input contains comments (for strict mode validation)
144    fn check_for_comments(input: &str) -> Result<(), LnmpError> {
145        // Check if input contains comment lines (lines starting with #)
146        // Note: # after a value is a checksum, not a comment
147        for (line_idx, line) in input.lines().enumerate() {
148            let trimmed = line.trim();
149            // If line starts with #, it's a comment
150            if trimmed.starts_with('#') {
151                return Err(LnmpError::StrictModeViolation {
152                    reason: "Comments are not allowed in strict mode".to_string(),
153                    line: line_idx + 1,
154                    column: 1,
155                });
156            }
157        }
158        Ok(())
159    }
160}
161
162impl<'a> Parser<'a> {
163    /// Parses a field ID (expects F prefix followed by number)
164    fn parse_field_id(&mut self) -> Result<FieldId, LnmpError> {
165        let (line, column) = self.lexer.position_original();
166
167        // Expect F prefix
168        self.expect(Token::FieldPrefix)?;
169
170        // Expect number
171        match &self.current_token {
172            Token::Number(num_str) => {
173                let num_str = num_str.clone();
174                self.advance()?;
175
176                // Parse as u16
177                match num_str.parse::<u16>() {
178                    Ok(fid) => Ok(fid),
179                    Err(_) => Err(LnmpError::InvalidFieldId {
180                        value: num_str,
181                        line,
182                        column,
183                    }),
184                }
185            }
186            Token::UnquotedString(s) => {
187                // 'F' followed by non-numeric characters - invalid field id
188                Err(LnmpError::InvalidFieldId {
189                    value: s.clone(),
190                    line,
191                    column,
192                })
193            }
194            _ => Err(LnmpError::UnexpectedToken {
195                expected: "field ID number".to_string(),
196                found: self.current_token.clone(),
197                line,
198                column,
199            }),
200        }
201    }
202
203    /// Parses a value based on the current token
204    #[allow(dead_code)]
205    fn parse_value(&mut self) -> Result<LnmpValue, LnmpError> {
206        self.parse_value_with_hint(None)
207    }
208
209    /// Parses a value with an optional type hint to resolve ambiguities
210    fn parse_value_with_hint(
211        &mut self,
212        type_hint: Option<TypeHint>,
213    ) -> Result<LnmpValue, LnmpError> {
214        let (line, column) = self.lexer.position_original();
215
216        match &self.current_token {
217            Token::Number(num_str) => {
218                let num_str = num_str.clone();
219                self.advance()?;
220                // If the type hint is a boolean, enforce boolean parsing for 0/1
221                if type_hint == Some(TypeHint::Bool) {
222                    if num_str == "0" {
223                        return Ok(LnmpValue::Bool(false));
224                    } else if num_str == "1" {
225                        return Ok(LnmpValue::Bool(true));
226                    } else {
227                        return Err(LnmpError::InvalidValue {
228                            field_id: 0,
229                            reason: format!("invalid boolean value: {}", num_str),
230                            line,
231                            column,
232                        });
233                    }
234                }
235
236                // If normalization is enabled, interpret 0/1 as booleans but do not reject other numbers
237                if self.config.normalize_values {
238                    if num_str == "0" {
239                        return Ok(LnmpValue::Bool(false));
240                    } else if num_str == "1" {
241                        return Ok(LnmpValue::Bool(true));
242                    }
243                }
244
245                // Try to parse as float if it contains a dot
246                if num_str.contains('.') {
247                    match num_str.parse::<f64>() {
248                        Ok(f) => Ok(LnmpValue::Float(f)),
249                        Err(_) => Err(LnmpError::InvalidValue {
250                            field_id: 0,
251                            reason: format!("invalid float: {}", num_str),
252                            line,
253                            column,
254                        }),
255                    }
256                } else {
257                    // Parse as integer
258                    match num_str.parse::<i64>() {
259                        Ok(i) => Ok(LnmpValue::Int(i)),
260                        Err(_) => Err(LnmpError::InvalidValue {
261                            field_id: 0,
262                            reason: format!("invalid integer: {}", num_str),
263                            line,
264                            column,
265                        }),
266                    }
267                }
268            }
269            Token::QuotedString(s) => {
270                let s = s.clone();
271                self.advance()?;
272                Ok(LnmpValue::String(s))
273            }
274            Token::UnquotedString(s) => {
275                let s = s.clone();
276                self.advance()?;
277                // If a boolean type hint is present or normalization is enabled, allow
278                // text values 'true'/'false' or 'yes'/'no' to be interpreted as booleans.
279                if type_hint == Some(TypeHint::Bool) || self.config.normalize_values {
280                    match s.to_ascii_lowercase().as_str() {
281                        "true" | "yes" => return Ok(LnmpValue::Bool(true)),
282                        "false" | "no" => return Ok(LnmpValue::Bool(false)),
283                        _ => {}
284                    }
285                }
286                Ok(LnmpValue::String(s))
287            }
288            Token::LeftBracket => self.parse_string_array_or_nested_array_with_hint(type_hint),
289            Token::LeftBrace => self.parse_nested_record(),
290            _ => Err(LnmpError::UnexpectedToken {
291                expected: "value".to_string(),
292                found: self.current_token.clone(),
293                line,
294                column,
295            }),
296        }
297    }
298
299    /// Parses either a string array [item1, item2, ...] or nested array [{...}, {...}]
300    #[allow(dead_code)]
301    fn parse_string_array_or_nested_array(&mut self) -> Result<LnmpValue, LnmpError> {
302        self.parse_string_array_or_nested_array_with_hint(None)
303    }
304
305    /// Parses either a string array or nested array with optional type hint
306    fn parse_string_array_or_nested_array_with_hint(
307        &mut self,
308        type_hint: Option<TypeHint>,
309    ) -> Result<LnmpValue, LnmpError> {
310        self.expect(Token::LeftBracket)?;
311
312        // Handle empty array - use type hint to determine type
313        if self.current_token == Token::RightBracket {
314            self.advance()?;
315            // If type hint is RecordArray, return empty NestedArray
316            if type_hint == Some(TypeHint::RecordArray) {
317                return Ok(LnmpValue::NestedArray(Vec::new()));
318            }
319            // Otherwise default to StringArray
320            return Ok(LnmpValue::StringArray(Vec::new()));
321        }
322
323        // Check if it's a nested array (starts with '{') or string array
324        if self.current_token == Token::LeftBrace {
325            self.parse_nested_array()
326        } else {
327            self.parse_string_array()
328        }
329    }
330
331    /// Parses a string array [item1, item2, ...]
332    fn parse_string_array(&mut self) -> Result<LnmpValue, LnmpError> {
333        let (line, column) = self.lexer.position_original();
334        let mut items = Vec::new();
335
336        loop {
337            // Parse string item
338            match &self.current_token {
339                Token::QuotedString(s) => {
340                    items.push(s.clone());
341                    self.advance()?;
342                }
343                Token::UnquotedString(s) => {
344                    items.push(s.clone());
345                    self.advance()?;
346                }
347                _ => {
348                    return Err(LnmpError::UnexpectedToken {
349                        expected: "string".to_string(),
350                        found: self.current_token.clone(),
351                        line,
352                        column,
353                    });
354                }
355            }
356
357            // Check for comma or closing bracket
358            match &self.current_token {
359                Token::Comma => {
360                    self.advance()?;
361                    // Continue to next item
362                }
363                Token::RightBracket => {
364                    self.advance()?;
365                    break;
366                }
367                _ => {
368                    return Err(LnmpError::UnexpectedToken {
369                        expected: "comma or closing bracket".to_string(),
370                        found: self.current_token.clone(),
371                        line,
372                        column,
373                    });
374                }
375            }
376        }
377
378        Ok(LnmpValue::StringArray(items))
379    }
380
381    /// Parses a nested record {F<id>=<value>;F<id>=<value>}
382    fn parse_nested_record(&mut self) -> Result<LnmpValue, LnmpError> {
383        let (line, column) = self.lexer.position_original();
384        self.expect(Token::LeftBrace)?;
385
386        // Increase nesting depth and enforce maximum if configured
387        self.nesting_depth += 1;
388        if let Some(max) = self.config.max_nesting_depth {
389            if self.nesting_depth > max {
390                let actual = self.nesting_depth;
391                self.nesting_depth = self.nesting_depth.saturating_sub(1);
392                return Err(LnmpError::NestingTooDeep {
393                    max_depth: max,
394                    actual_depth: actual,
395                    line,
396                    column,
397                });
398            }
399        }
400
401        // Use a closure to ensure we can decrement nesting_depth on all return paths
402        let result = (|| -> Result<LnmpValue, LnmpError> {
403            let mut record = LnmpRecord::new();
404
405            // Handle empty nested record
406            if self.current_token == Token::RightBrace {
407                self.advance()?;
408                return Ok(LnmpValue::NestedRecord(Box::new(record)));
409            }
410
411            // Parse field assignments within the nested record
412            loop {
413                let field = self.parse_field_assignment()?;
414                // In strict mode, detect duplicate field IDs in nested records and error early
415                if self.config.mode == ParsingMode::Strict && record.get_field(field.fid).is_some()
416                {
417                    let (line, column) = self.lexer.position_original();
418                    return Err(LnmpError::DuplicateFieldId {
419                        field_id: field.fid,
420                        line,
421                        column,
422                    });
423                }
424                record.add_field(field);
425
426                // Check for separator or closing brace
427                match &self.current_token {
428                    Token::Semicolon => {
429                        self.advance()?;
430                        // Check if we're at the end
431                        if self.current_token == Token::RightBrace {
432                            self.advance()?;
433                            break;
434                        }
435                        // Continue to next field
436                    }
437                    Token::RightBrace => {
438                        self.advance()?;
439                        break;
440                    }
441                    _ => {
442                        return Err(LnmpError::UnexpectedToken {
443                            expected: "semicolon or closing brace".to_string(),
444                            found: self.current_token.clone(),
445                            line,
446                            column,
447                        });
448                    }
449                }
450            }
451
452            // Sort fields by FID for canonical representation
453            let sorted_record = LnmpRecord::from_sorted_fields(record.sorted_fields());
454
455            Ok(LnmpValue::NestedRecord(Box::new(sorted_record)))
456        })();
457
458        // Always decrement nesting depth before returning
459        self.nesting_depth = self.nesting_depth.saturating_sub(1);
460        result
461    }
462
463    /// Parses a nested array [{...}, {...}]
464    fn parse_nested_array(&mut self) -> Result<LnmpValue, LnmpError> {
465        let (line, column) = self.lexer.position_original();
466
467        // Increase nesting depth for nested array
468        self.nesting_depth += 1;
469        if let Some(max) = self.config.max_nesting_depth {
470            if self.nesting_depth > max {
471                let actual = self.nesting_depth;
472                self.nesting_depth = self.nesting_depth.saturating_sub(1);
473                return Err(LnmpError::NestingTooDeep {
474                    max_depth: max,
475                    actual_depth: actual,
476                    line,
477                    column,
478                });
479            }
480        }
481
482        let result = (|| -> Result<LnmpValue, LnmpError> {
483            let mut records = Vec::new();
484
485            loop {
486                // Parse nested record
487                if self.current_token != Token::LeftBrace {
488                    return Err(LnmpError::UnexpectedToken {
489                        expected: "left brace for nested record".to_string(),
490                        found: self.current_token.clone(),
491                        line,
492                        column,
493                    });
494                }
495
496                // Parse the nested record value and extract the record
497                match self.parse_nested_record()? {
498                    LnmpValue::NestedRecord(record) => {
499                        records.push(*record);
500                    }
501                    _ => unreachable!("parse_nested_record always returns NestedRecord"),
502                }
503
504                // Check for comma or closing bracket
505                match &self.current_token {
506                    Token::Comma => {
507                        self.advance()?;
508                        // Continue to next record
509                    }
510                    Token::RightBracket => {
511                        self.advance()?;
512                        break;
513                    }
514                    _ => {
515                        return Err(LnmpError::UnexpectedToken {
516                            expected: "comma or closing bracket".to_string(),
517                            found: self.current_token.clone(),
518                            line,
519                            column,
520                        });
521                    }
522                }
523            }
524
525            Ok(LnmpValue::NestedArray(records))
526        })();
527
528        // Leaving nesting: decrement depth
529        self.nesting_depth = self.nesting_depth.saturating_sub(1);
530        result
531    }
532
533    /// Parses a type hint (optional :type after field ID)
534    fn parse_type_hint(&mut self) -> Result<Option<TypeHint>, LnmpError> {
535        if let Token::TypeHint(hint_str) = &self.current_token {
536            let hint_str = hint_str.clone();
537            self.advance()?;
538
539            match TypeHint::parse(&hint_str) {
540                Some(hint) => Ok(Some(hint)),
541                None => {
542                    let (line, column) = self.lexer.position_original();
543                    Err(LnmpError::InvalidTypeHint {
544                        hint: hint_str,
545                        line,
546                        column,
547                    })
548                }
549            }
550        } else {
551            Ok(None)
552        }
553    }
554
555    /// Parses a field assignment (F<id>=<value> or F<id>:<type>=<value>)
556    fn parse_field_assignment(&mut self) -> Result<LnmpField, LnmpError> {
557        let fid = self.parse_field_id()?;
558
559        // Check for optional type hint
560        let type_hint = self.parse_type_hint()?;
561
562        self.expect(Token::Equals)?;
563        let value = self.parse_value_with_hint(type_hint)?;
564
565        // Validate type hint if present
566        if let Some(hint) = type_hint {
567            if !hint.validates(&value) {
568                let (line, column) = self.lexer.position_original();
569                return Err(LnmpError::TypeHintMismatch {
570                    field_id: fid,
571                    expected_type: hint.as_str().to_string(),
572                    actual_value: format!("{:?}", value),
573                    line,
574                    column,
575                });
576            }
577        }
578
579        // Check for optional checksum
580        if self.current_token == Token::Hash {
581            self.parse_and_validate_checksum(fid, type_hint, &value)?;
582        } else if self.config.require_checksums {
583            let (line, column) = self.lexer.position_original();
584            return Err(LnmpError::ChecksumMismatch {
585                field_id: fid,
586                expected: "checksum required".to_string(),
587                found: "no checksum".to_string(),
588                line,
589                column,
590            });
591        }
592
593        // Apply semantic normalization if configured
594        let normalized_value = if let Some(norm) = &self.normalizer {
595            norm.normalize_with_fid(Some(fid), &value)
596        } else {
597            value
598        };
599
600        Ok(LnmpField {
601            fid,
602            value: normalized_value,
603        })
604    }
605
606    /// Parses and validates a checksum
607    fn parse_and_validate_checksum(
608        &mut self,
609        fid: FieldId,
610        type_hint: Option<TypeHint>,
611        value: &LnmpValue,
612    ) -> Result<(), LnmpError> {
613        let (line, column) = self.lexer.position_original();
614
615        // Consume the hash token
616        self.expect(Token::Hash)?;
617
618        // Read the checksum - it might be split across multiple tokens
619        // (e.g., "36AAE667" might be tokenized as Number("36") + UnquotedString("AAE") + Number("667"))
620        let mut checksum_str = String::new();
621
622        // Collect all tokens until we hit a separator (newline, semicolon, EOF)
623        loop {
624            match &self.current_token {
625                Token::Number(s) => {
626                    checksum_str.push_str(s);
627                    self.advance()?;
628                }
629                Token::UnquotedString(s) => {
630                    checksum_str.push_str(s);
631                    self.advance()?;
632                }
633                Token::Newline | Token::Semicolon | Token::Eof => {
634                    break;
635                }
636                _ => {
637                    return Err(LnmpError::UnexpectedToken {
638                        expected: "checksum (8 hex characters)".to_string(),
639                        found: self.current_token.clone(),
640                        line,
641                        column,
642                    });
643                }
644            }
645
646            // Stop after 8 characters
647            if checksum_str.len() >= 8 {
648                break;
649            }
650        }
651
652        // Parse the checksum
653        let provided_checksum =
654            SemanticChecksum::parse(&checksum_str).ok_or_else(|| LnmpError::InvalidChecksum {
655                field_id: fid,
656                reason: format!("invalid checksum format: {}", checksum_str),
657                line,
658                column,
659            })?;
660
661        // Validate checksum if enabled
662        if self.config.validate_checksums {
663            let computed_checksum = SemanticChecksum::compute(fid, type_hint, value);
664            if provided_checksum != computed_checksum {
665                return Err(LnmpError::ChecksumMismatch {
666                    field_id: fid,
667                    expected: SemanticChecksum::format(computed_checksum),
668                    found: SemanticChecksum::format(provided_checksum),
669                    line,
670                    column,
671                });
672            }
673        }
674
675        Ok(())
676    }
677
678    /// Validates that fields are sorted by FID (strict mode only)
679    fn validate_field_order(&self, record: &LnmpRecord) -> Result<(), LnmpError> {
680        let fields = record.fields();
681        for i in 1..fields.len() {
682            if fields[i].fid < fields[i - 1].fid {
683                let (line, column) = self.lexer.position_original();
684                return Err(LnmpError::StrictModeViolation {
685                    reason: format!(
686                        "Fields must be sorted by FID in strict mode (F{} appears after F{})",
687                        fields[i].fid,
688                        fields[i - 1].fid
689                    ),
690                    line,
691                    column,
692                });
693            }
694        }
695        Ok(())
696    }
697
698    // Duplicate field IDs are now detected during parsing and a DuplicateFieldId
699    // error is emitted at parse time with an accurate lexer position.
700
701    /// Validates separator (strict mode rejects semicolons)
702    fn validate_separator(&self, is_semicolon: bool) -> Result<(), LnmpError> {
703        if self.config.mode == ParsingMode::Strict && is_semicolon {
704            let (line, column) = self.lexer.position_original();
705            return Err(LnmpError::StrictModeViolation {
706                reason: "Semicolons are not allowed in strict mode (use newlines)".to_string(),
707                line,
708                column,
709            });
710        }
711        Ok(())
712    }
713
714    /// Parses a complete record
715    pub fn parse_record(&mut self) -> Result<LnmpRecord, LnmpError> {
716        let mut record = LnmpRecord::new();
717
718        // Skip leading newlines and comments
719        self.skip_newlines()?;
720        while self.current_token == Token::Hash {
721            self.skip_comment()?;
722            if self.current_token == Token::Newline {
723                self.advance()?;
724            }
725            self.skip_newlines()?;
726        }
727
728        // Parse field assignments until EOF
729        while self.current_token != Token::Eof {
730            let field = self.parse_field_assignment()?;
731            // In strict mode, detect duplicate field IDs and error early
732            if self.config.mode == ParsingMode::Strict && record.get_field(field.fid).is_some() {
733                let (line, column) = self.lexer.position_original();
734                return Err(LnmpError::DuplicateFieldId {
735                    field_id: field.fid,
736                    line,
737                    column,
738                });
739            }
740            record.add_field(field);
741
742            // Handle separator (semicolon or newline)
743            match &self.current_token {
744                Token::Semicolon => {
745                    self.validate_separator(true)?;
746                    self.advance()?;
747                }
748                Token::Newline => {
749                    self.advance()?;
750                    self.skip_newlines()?;
751                    // Skip comments after newlines
752                    while self.current_token == Token::Hash {
753                        self.skip_comment()?;
754                        if self.current_token == Token::Newline {
755                            self.advance()?;
756                        }
757                        self.skip_newlines()?;
758                    }
759                }
760                Token::Eof => break,
761                _ => {
762                    let (line, column) = self.lexer.position_original();
763                    return Err(LnmpError::UnexpectedToken {
764                        expected: "semicolon, newline, or EOF".to_string(),
765                        found: self.current_token.clone(),
766                        line,
767                        column,
768                    });
769                }
770            }
771        }
772
773        // Validate field order and duplicate field IDs in strict mode
774        if self.config.mode == ParsingMode::Strict {
775            self.validate_field_order(&record)?;
776        }
777
778        // Enforce structural limits if provided
779        if let Some(limits) = &self.config.structural_limits {
780            if let Err(err) = limits.validate_record(&record) {
781                let (line, column) = self.lexer.position_original();
782                return Err(LnmpError::InvalidNestedStructure {
783                    reason: format!("structural limits violated: {}", err),
784                    line,
785                    column,
786                });
787            }
788        }
789
790        Ok(record)
791    }
792}
793
794#[cfg(test)]
795#[allow(clippy::approx_constant)]
796mod tests {
797    use super::*;
798
799    #[test]
800    fn test_parse_integer() {
801        let mut parser = Parser::new("F1=42").unwrap();
802        let record = parser.parse_record().unwrap();
803        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
804    }
805
806    #[test]
807    fn test_parse_negative_integer() {
808        let mut parser = Parser::new("F1=-123").unwrap();
809        let record = parser.parse_record().unwrap();
810        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(-123));
811    }
812
813    #[test]
814    fn test_parse_float() {
815        let mut parser = Parser::new("F2=3.14").unwrap();
816        let record = parser.parse_record().unwrap();
817        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
818    }
819
820    #[test]
821    fn test_parse_bool_true() {
822        let mut parser = Parser::new("F3=1").unwrap();
823        let record = parser.parse_record().unwrap();
824        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
825    }
826
827    #[test]
828    fn test_parse_bool_false() {
829        let mut parser = Parser::new("F3=0").unwrap();
830        let record = parser.parse_record().unwrap();
831        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(false));
832    }
833
834    #[test]
835    fn test_duplicate_field_id_strict_mode_error() {
836        let mut parser = Parser::with_mode("F1=1\nF1=2", ParsingMode::Strict).unwrap();
837        let err = parser.parse_record().unwrap_err();
838        match err {
839            LnmpError::DuplicateFieldId { field_id, .. } => {
840                assert_eq!(field_id, 1);
841            }
842            _ => panic!("expected DuplicateFieldId error, got: {:?}", err),
843        }
844    }
845
846    #[test]
847    fn test_duplicate_field_id_loose_mode_allows() {
848        let mut parser = Parser::new("F1=1;F1=2").unwrap();
849        let record = parser.parse_record().unwrap();
850        let fields = record.fields();
851        assert_eq!(fields.len(), 2);
852        assert_eq!(fields[0].fid, 1);
853        assert_eq!(fields[1].fid, 1);
854    }
855
856    #[test]
857    fn test_duplicate_field_id_in_nested_record_strict_mode_error() {
858        let mut parser = Parser::with_mode("F50={F1=1;F1=2}", ParsingMode::Strict).unwrap();
859        let err = parser.parse_record().unwrap_err();
860        match err {
861            LnmpError::DuplicateFieldId { field_id, .. } => {
862                assert_eq!(field_id, 1);
863            }
864            _ => panic!("expected DuplicateFieldId error, got: {:?}", err),
865        }
866    }
867
868    #[test]
869    fn test_duplicate_field_id_in_nested_record_loose_mode_allows() {
870        let mut parser = Parser::new("F50={F1=1;F1=2}").unwrap();
871        let record = parser.parse_record().unwrap();
872        let nested = record.get_field(50).unwrap();
873        if let LnmpValue::NestedRecord(nested_record) = &nested.value {
874            assert_eq!(nested_record.fields().len(), 2);
875            assert_eq!(nested_record.fields()[0].fid, 1);
876            assert_eq!(nested_record.fields()[1].fid, 1);
877        } else {
878            panic!("expected nested record");
879        }
880    }
881
882    #[test]
883    fn test_parse_quoted_string() {
884        let mut parser = Parser::new(r#"F4="hello world""#).unwrap();
885        let record = parser.parse_record().unwrap();
886        assert_eq!(
887            record.get_field(4).unwrap().value,
888            LnmpValue::String("hello world".to_string())
889        );
890    }
891
892    #[test]
893    fn test_parse_unquoted_string() {
894        let mut parser = Parser::new("F5=test_value").unwrap();
895        let record = parser.parse_record().unwrap();
896        assert_eq!(
897            record.get_field(5).unwrap().value,
898            LnmpValue::String("test_value".to_string())
899        );
900    }
901
902    #[test]
903    fn test_parse_string_array() {
904        let mut parser = Parser::new(r#"F6=["admin","dev","user"]"#).unwrap();
905        let record = parser.parse_record().unwrap();
906        assert_eq!(
907            record.get_field(6).unwrap().value,
908            LnmpValue::StringArray(vec![
909                "admin".to_string(),
910                "dev".to_string(),
911                "user".to_string()
912            ])
913        );
914    }
915
916    #[test]
917    fn test_parse_empty_string_array() {
918        let mut parser = Parser::new("F6=[]").unwrap();
919        let record = parser.parse_record().unwrap();
920        assert_eq!(
921            record.get_field(6).unwrap().value,
922            LnmpValue::StringArray(vec![])
923        );
924    }
925
926    #[test]
927    fn test_parse_multiline_record() {
928        let input = "F12=14532\nF7=1\nF20=\"Halil\"";
929        let mut parser = Parser::new(input).unwrap();
930        let record = parser.parse_record().unwrap();
931
932        assert_eq!(record.fields().len(), 3);
933        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
934        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
935        assert_eq!(
936            record.get_field(20).unwrap().value,
937            LnmpValue::String("Halil".to_string())
938        );
939    }
940
941    #[test]
942    fn test_parse_inline_record() {
943        let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
944        let mut parser = Parser::new(input).unwrap();
945        let record = parser.parse_record().unwrap();
946
947        assert_eq!(record.fields().len(), 3);
948        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
949        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
950        assert_eq!(
951            record.get_field(23).unwrap().value,
952            LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
953        );
954    }
955
956    #[test]
957    fn test_parse_with_comments() {
958        let input = "# This is a comment\nF1=42\n# Another comment\nF2=3.14";
959        let mut parser = Parser::new(input).unwrap();
960        let record = parser.parse_record().unwrap();
961
962        assert_eq!(record.fields().len(), 2);
963        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
964        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
965    }
966
967    #[test]
968    fn test_parse_with_whitespace() {
969        let input = "F1  =  42  ;  F2  =  3.14";
970        let mut parser = Parser::new(input).unwrap();
971        let record = parser.parse_record().unwrap();
972
973        assert_eq!(record.fields().len(), 2);
974        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
975        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
976    }
977
978    #[test]
979    fn test_parse_empty_input() {
980        let mut parser = Parser::new("").unwrap();
981        let record = parser.parse_record().unwrap();
982        assert_eq!(record.fields().len(), 0);
983    }
984
985    #[test]
986    fn test_parse_only_comments() {
987        let input = "# Comment 1\n# Comment 2\n# Comment 3";
988        let mut parser = Parser::new(input).unwrap();
989        let record = parser.parse_record().unwrap();
990        assert_eq!(record.fields().len(), 0);
991    }
992
993    #[test]
994    fn test_parse_field_id_out_of_range() {
995        let result = Parser::new("F99999=42");
996        assert!(result.is_ok());
997        let mut parser = result.unwrap();
998        let result = parser.parse_record();
999        assert!(result.is_err());
1000        match result {
1001            Err(LnmpError::InvalidFieldId { .. }) => {}
1002            _ => panic!("Expected InvalidFieldId error"),
1003        }
1004    }
1005
1006    #[test]
1007    fn test_parse_missing_equals() {
1008        let mut parser = Parser::new("F1 42").unwrap();
1009        let result = parser.parse_record();
1010        assert!(result.is_err());
1011        match result {
1012            Err(LnmpError::UnexpectedToken { .. }) => {}
1013            _ => panic!("Expected UnexpectedToken error"),
1014        }
1015    }
1016
1017    #[test]
1018    fn test_parse_missing_value() {
1019        let mut parser = Parser::new("F1=").unwrap();
1020        let result = parser.parse_record();
1021        assert!(result.is_err());
1022    }
1023
1024    #[test]
1025    fn test_parse_invalid_field_prefix() {
1026        let result = Parser::new("G1=42");
1027        assert!(result.is_ok());
1028        let mut parser = result.unwrap();
1029        let result = parser.parse_record();
1030        assert!(result.is_err());
1031    }
1032
1033    #[test]
1034    fn test_parse_string_with_escapes() {
1035        let input = r#"F1="hello \"world\"""#;
1036        let mut parser = Parser::new(input).unwrap();
1037        let record = parser.parse_record().unwrap();
1038        assert_eq!(
1039            record.get_field(1).unwrap().value,
1040            LnmpValue::String("hello \"world\"".to_string())
1041        );
1042    }
1043
1044    #[test]
1045    fn test_parse_mixed_separators() {
1046        let input = "F1=1;F2=2\nF3=3;F4=4";
1047        let mut parser = Parser::new(input).unwrap();
1048        let record = parser.parse_record().unwrap();
1049        assert_eq!(record.fields().len(), 4);
1050    }
1051
1052    #[test]
1053    fn test_parse_large_field_id() {
1054        let mut parser = Parser::new("F65535=42").unwrap();
1055        let record = parser.parse_record().unwrap();
1056        assert_eq!(record.get_field(65535).unwrap().value, LnmpValue::Int(42));
1057    }
1058
1059    #[test]
1060    fn test_parse_spec_example() {
1061        let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
1062        let mut parser = Parser::new(input).unwrap();
1063        let record = parser.parse_record().unwrap();
1064
1065        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1066        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1067        assert_eq!(
1068            record.get_field(23).unwrap().value,
1069            LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1070        );
1071    }
1072
1073    #[test]
1074    fn test_strict_mode_rejects_unsorted_fields() {
1075        use crate::config::ParsingMode;
1076
1077        // Unsorted fields: F12, F7, F23
1078        let input = r#"F12=14532
1079F7=1
1080F23=["admin","dev"]"#;
1081
1082        let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1083        let result = parser.parse_record();
1084
1085        assert!(result.is_err());
1086        match result {
1087            Err(LnmpError::StrictModeViolation { reason, .. }) => {
1088                assert!(reason.contains("sorted"));
1089            }
1090            _ => panic!("Expected StrictModeViolation error"),
1091        }
1092    }
1093
1094    #[test]
1095    fn test_strict_mode_accepts_sorted_fields() {
1096        use crate::config::ParsingMode;
1097
1098        // Sorted fields: F7, F12, F23
1099        let input = r#"F7=1
1100F12=14532
1101F23=["admin","dev"]"#;
1102
1103        let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1104        let record = parser.parse_record().unwrap();
1105
1106        assert_eq!(record.fields().len(), 3);
1107        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1108        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1109    }
1110
1111    #[test]
1112    fn test_strict_mode_rejects_semicolons() {
1113        use crate::config::ParsingMode;
1114
1115        let input = "F1=1;F2=2";
1116
1117        let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1118        let result = parser.parse_record();
1119
1120        assert!(result.is_err());
1121        match result {
1122            Err(LnmpError::StrictModeViolation { reason, .. }) => {
1123                assert!(reason.contains("Semicolons"));
1124            }
1125            _ => panic!("Expected StrictModeViolation error"),
1126        }
1127    }
1128
1129    #[test]
1130    fn test_strict_mode_rejects_comments() {
1131        use crate::config::ParsingMode;
1132
1133        let input = "# This is a comment\nF1=42";
1134
1135        let result = Parser::with_mode(input, ParsingMode::Strict);
1136
1137        assert!(result.is_err());
1138        match result {
1139            Err(LnmpError::StrictModeViolation { reason, .. }) => {
1140                assert!(reason.contains("Comments"));
1141            }
1142            _ => panic!("Expected StrictModeViolation error"),
1143        }
1144    }
1145
1146    #[test]
1147    fn test_loose_mode_accepts_unsorted_fields() {
1148        use crate::config::ParsingMode;
1149
1150        // Unsorted fields
1151        let input = r#"F23=["admin","dev"]
1152F7=1
1153F12=14532"#;
1154
1155        let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1156        let record = parser.parse_record().unwrap();
1157
1158        assert_eq!(record.fields().len(), 3);
1159        // Fields are in insertion order, not sorted
1160        assert_eq!(record.fields()[0].fid, 23);
1161        assert_eq!(record.fields()[1].fid, 7);
1162        assert_eq!(record.fields()[2].fid, 12);
1163    }
1164
1165    #[test]
1166    fn test_loose_mode_accepts_semicolons() {
1167        use crate::config::ParsingMode;
1168
1169        let input = "F1=1;F2=2;F3=3";
1170
1171        let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1172        let record = parser.parse_record().unwrap();
1173
1174        assert_eq!(record.fields().len(), 3);
1175        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Bool(true));
1176        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(2));
1177        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Int(3));
1178    }
1179
1180    #[test]
1181    fn test_loose_mode_accepts_whitespace() {
1182        use crate::config::ParsingMode;
1183
1184        let input = "F1  =  42  ;  F2  =  3.14";
1185
1186        let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1187        let record = parser.parse_record().unwrap();
1188
1189        assert_eq!(record.fields().len(), 2);
1190        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1191        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1192    }
1193
1194    #[test]
1195    fn test_loose_mode_accepts_comments() {
1196        use crate::config::ParsingMode;
1197
1198        let input = "# Comment\nF1=42\n# Another comment\nF2=3.14";
1199
1200        let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1201        let record = parser.parse_record().unwrap();
1202
1203        assert_eq!(record.fields().len(), 2);
1204        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1205        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1206    }
1207
1208    #[test]
1209    fn test_default_mode_is_loose() {
1210        let input = "F2=2;F1=1"; // Unsorted with semicolons
1211
1212        let mut parser = Parser::new(input).unwrap();
1213        assert_eq!(parser.mode(), ParsingMode::Loose);
1214
1215        let record = parser.parse_record().unwrap();
1216        assert_eq!(record.fields().len(), 2);
1217    }
1218
1219    #[test]
1220    fn test_strict_mode_with_sorted_no_semicolons() {
1221        use crate::config::ParsingMode;
1222
1223        let input = "F1=1\nF2=2\nF3=3";
1224
1225        let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1226        let record = parser.parse_record().unwrap();
1227
1228        assert_eq!(record.fields().len(), 3);
1229        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Bool(true));
1230        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(2));
1231        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Int(3));
1232    }
1233
1234    #[test]
1235    fn test_parse_type_hint_integer() {
1236        let input = "F12:i=14532";
1237        let mut parser = Parser::new(input).unwrap();
1238        let record = parser.parse_record().unwrap();
1239
1240        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1241    }
1242
1243    #[test]
1244    fn test_parse_type_hint_float() {
1245        let input = "F5:f=3.14";
1246        let mut parser = Parser::new(input).unwrap();
1247        let record = parser.parse_record().unwrap();
1248
1249        assert_eq!(record.get_field(5).unwrap().value, LnmpValue::Float(3.14));
1250    }
1251
1252    #[test]
1253    fn test_parse_type_hint_bool() {
1254        let input = "F7:b=1";
1255        let mut parser = Parser::new(input).unwrap();
1256        let record = parser.parse_record().unwrap();
1257
1258        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1259    }
1260
1261    #[test]
1262    fn test_parse_type_hint_string() {
1263        let input = r#"F10:s="test""#;
1264        let mut parser = Parser::new(input).unwrap();
1265        let record = parser.parse_record().unwrap();
1266
1267        assert_eq!(
1268            record.get_field(10).unwrap().value,
1269            LnmpValue::String("test".to_string())
1270        );
1271    }
1272
1273    #[test]
1274    fn test_parse_type_hint_string_array() {
1275        let input = r#"F23:sa=["admin","dev"]"#;
1276        let mut parser = Parser::new(input).unwrap();
1277        let record = parser.parse_record().unwrap();
1278
1279        assert_eq!(
1280            record.get_field(23).unwrap().value,
1281            LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1282        );
1283    }
1284
1285    #[test]
1286    fn test_parse_all_type_hints() {
1287        let input = r#"F1:i=42
1288F2:f=3.14
1289F3:b=1
1290F4:s=test
1291F5:sa=[a,b]"#;
1292        let mut parser = Parser::new(input).unwrap();
1293        let record = parser.parse_record().unwrap();
1294
1295        assert_eq!(record.fields().len(), 5);
1296        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1297        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1298        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
1299        assert_eq!(
1300            record.get_field(4).unwrap().value,
1301            LnmpValue::String("test".to_string())
1302        );
1303        assert_eq!(
1304            record.get_field(5).unwrap().value,
1305            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1306        );
1307    }
1308
1309    #[test]
1310    fn test_type_hint_mismatch_int_vs_float() {
1311        let input = "F1:i=3.14"; // Type hint says int, but value is float
1312        let mut parser = Parser::new(input).unwrap();
1313        let result = parser.parse_record();
1314
1315        assert!(result.is_err());
1316        match result {
1317            Err(LnmpError::TypeHintMismatch {
1318                field_id,
1319                expected_type,
1320                ..
1321            }) => {
1322                assert_eq!(field_id, 1);
1323                assert_eq!(expected_type, "i");
1324            }
1325            _ => panic!("Expected TypeHintMismatch error"),
1326        }
1327    }
1328
1329    #[test]
1330    fn test_type_hint_mismatch_float_vs_int() {
1331        let input = "F2:f=42"; // Type hint says float, but value is int
1332        let mut parser = Parser::new(input).unwrap();
1333        let result = parser.parse_record();
1334
1335        assert!(result.is_err());
1336        match result {
1337            Err(LnmpError::TypeHintMismatch {
1338                field_id,
1339                expected_type,
1340                ..
1341            }) => {
1342                assert_eq!(field_id, 2);
1343                assert_eq!(expected_type, "f");
1344            }
1345            _ => panic!("Expected TypeHintMismatch error"),
1346        }
1347    }
1348
1349    #[test]
1350    fn test_type_hint_mismatch_string_vs_int() {
1351        let input = "F3:s=42"; // Type hint says string, but value is int
1352        let mut parser = Parser::new(input).unwrap();
1353        let result = parser.parse_record();
1354
1355        assert!(result.is_err());
1356        match result {
1357            Err(LnmpError::TypeHintMismatch { field_id, .. }) => {
1358                assert_eq!(field_id, 3);
1359            }
1360            _ => panic!("Expected TypeHintMismatch error"),
1361        }
1362    }
1363
1364    #[test]
1365    fn test_invalid_type_hint() {
1366        let input = "F1:xyz=42"; // Invalid type hint
1367        let mut parser = Parser::new(input).unwrap();
1368        let result = parser.parse_record();
1369
1370        assert!(result.is_err());
1371        match result {
1372            Err(LnmpError::InvalidTypeHint { hint, .. }) => {
1373                assert_eq!(hint, "xyz");
1374            }
1375            _ => panic!("Expected InvalidTypeHint error"),
1376        }
1377    }
1378
1379    #[test]
1380    fn test_field_without_type_hint_still_works() {
1381        let input = "F1=42\nF2:i=100";
1382        let mut parser = Parser::new(input).unwrap();
1383        let record = parser.parse_record().unwrap();
1384
1385        assert_eq!(record.fields().len(), 2);
1386        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1387        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(100));
1388    }
1389
1390    #[test]
1391    fn test_type_hint_with_whitespace() {
1392        let input = "F12 :i =14532";
1393        let mut parser = Parser::new(input).unwrap();
1394        let record = parser.parse_record().unwrap();
1395
1396        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1397    }
1398
1399    #[test]
1400    fn test_comment_lines_ignored_in_loose_mode() {
1401        let input = "# This is a comment\nF1=42\n# Another comment\nF2=100";
1402        let mut parser = Parser::new(input).unwrap();
1403        let record = parser.parse_record().unwrap();
1404
1405        assert_eq!(record.fields().len(), 2);
1406        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1407        assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(100));
1408    }
1409
1410    #[test]
1411    fn test_hash_in_quoted_string_preserved() {
1412        let input = r#"F1="This # is not a comment""#;
1413        let mut parser = Parser::new(input).unwrap();
1414        let record = parser.parse_record().unwrap();
1415
1416        assert_eq!(
1417            record.get_field(1).unwrap().value,
1418            LnmpValue::String("This # is not a comment".to_string())
1419        );
1420    }
1421
1422    #[test]
1423    fn test_comment_after_whitespace() {
1424        let input = "   # Comment with leading whitespace\nF1=42";
1425        let mut parser = Parser::new(input).unwrap();
1426        let record = parser.parse_record().unwrap();
1427
1428        assert_eq!(record.fields().len(), 1);
1429        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1430    }
1431
1432    #[test]
1433    fn test_inline_comments_not_supported() {
1434        // Inline comments are not supported - the # would be part of the value
1435        // This test verifies that inline comments don't work as expected
1436        let input = "F1=test"; // No inline comment to test
1437        let mut parser = Parser::new(input).unwrap();
1438        let record = parser.parse_record().unwrap();
1439
1440        assert_eq!(
1441            record.get_field(1).unwrap().value,
1442            LnmpValue::String("test".to_string())
1443        );
1444    }
1445
1446    #[test]
1447    fn test_encoder_never_outputs_comments() {
1448        use crate::encoder::Encoder;
1449
1450        let mut record = LnmpRecord::new();
1451        record.add_field(LnmpField {
1452            fid: 1,
1453            value: LnmpValue::String("test".to_string()),
1454        });
1455
1456        let encoder = Encoder::new();
1457        let output = encoder.encode(&record);
1458
1459        // Verify output doesn't contain comment character
1460        assert!(!output.contains('#'));
1461        assert_eq!(output, "F1=test");
1462    }
1463
1464    #[test]
1465    fn test_multiple_comment_lines() {
1466        let input = "# Comment 1\n# Comment 2\n# Comment 3\nF1=42";
1467        let mut parser = Parser::new(input).unwrap();
1468        let record = parser.parse_record().unwrap();
1469
1470        assert_eq!(record.fields().len(), 1);
1471        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1472    }
1473
1474    #[test]
1475    fn test_comment_at_end_of_file() {
1476        let input = "F1=42\n# Comment at end";
1477        let mut parser = Parser::new(input).unwrap();
1478        let record = parser.parse_record().unwrap();
1479
1480        assert_eq!(record.fields().len(), 1);
1481        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1482    }
1483
1484    #[test]
1485    fn test_empty_comment_line() {
1486        let input = "#\nF1=42";
1487        let mut parser = Parser::new(input).unwrap();
1488        let record = parser.parse_record().unwrap();
1489
1490        assert_eq!(record.fields().len(), 1);
1491        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1492    }
1493
1494    // Nested record tests
1495    #[test]
1496    fn test_parse_simple_nested_record() {
1497        let input = "F50={F12=1;F7=1}";
1498        let mut parser = Parser::new(input).unwrap();
1499        let record = parser.parse_record().unwrap();
1500
1501        assert_eq!(record.fields().len(), 1);
1502        let field = record.get_field(50).unwrap();
1503
1504        match &field.value {
1505            LnmpValue::NestedRecord(nested) => {
1506                assert_eq!(nested.fields().len(), 2);
1507                // Fields should be sorted by FID
1508                assert_eq!(nested.fields()[0].fid, 7);
1509                assert_eq!(nested.fields()[0].value, LnmpValue::Bool(true));
1510                assert_eq!(nested.fields()[1].fid, 12);
1511                assert_eq!(nested.fields()[1].value, LnmpValue::Bool(true));
1512            }
1513            _ => panic!("Expected NestedRecord"),
1514        }
1515    }
1516
1517    #[test]
1518    fn test_parse_nested_record_with_various_types() {
1519        let input = r#"F50={F12=14532;F7=1;F23=["admin","dev"]}"#;
1520        let mut parser = Parser::new(input).unwrap();
1521        let record = parser.parse_record().unwrap();
1522
1523        assert_eq!(record.fields().len(), 1);
1524        let field = record.get_field(50).unwrap();
1525
1526        match &field.value {
1527            LnmpValue::NestedRecord(nested) => {
1528                assert_eq!(nested.fields().len(), 3);
1529                // Fields should be sorted: F7, F12, F23
1530                assert_eq!(nested.get_field(7).unwrap().value, LnmpValue::Bool(true));
1531                assert_eq!(nested.get_field(12).unwrap().value, LnmpValue::Int(14532));
1532                assert_eq!(
1533                    nested.get_field(23).unwrap().value,
1534                    LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1535                );
1536            }
1537            _ => panic!("Expected NestedRecord"),
1538        }
1539    }
1540
1541    #[test]
1542    fn test_parse_empty_nested_record() {
1543        let input = "F50={}";
1544        let mut parser = Parser::new(input).unwrap();
1545        let record = parser.parse_record().unwrap();
1546
1547        assert_eq!(record.fields().len(), 1);
1548        let field = record.get_field(50).unwrap();
1549
1550        match &field.value {
1551            LnmpValue::NestedRecord(nested) => {
1552                assert_eq!(nested.fields().len(), 0);
1553            }
1554            _ => panic!("Expected NestedRecord"),
1555        }
1556    }
1557
1558    #[test]
1559    fn test_parse_deeply_nested_record() {
1560        let input = "F100={F1=user;F2={F10=nested;F11=data}}";
1561        let mut parser = Parser::new(input).unwrap();
1562        let record = parser.parse_record().unwrap();
1563
1564        assert_eq!(record.fields().len(), 1);
1565        let field = record.get_field(100).unwrap();
1566
1567        match &field.value {
1568            LnmpValue::NestedRecord(nested) => {
1569                assert_eq!(nested.fields().len(), 2);
1570                assert_eq!(
1571                    nested.get_field(1).unwrap().value,
1572                    LnmpValue::String("user".to_string())
1573                );
1574
1575                // Check the nested record within
1576                match &nested.get_field(2).unwrap().value {
1577                    LnmpValue::NestedRecord(inner) => {
1578                        assert_eq!(inner.fields().len(), 2);
1579                        assert_eq!(
1580                            inner.get_field(10).unwrap().value,
1581                            LnmpValue::String("nested".to_string())
1582                        );
1583                        assert_eq!(
1584                            inner.get_field(11).unwrap().value,
1585                            LnmpValue::String("data".to_string())
1586                        );
1587                    }
1588                    _ => panic!("Expected nested NestedRecord"),
1589                }
1590            }
1591            _ => panic!("Expected NestedRecord"),
1592        }
1593    }
1594
1595    #[test]
1596    fn test_parse_nested_record_fields_sorted() {
1597        // Input has unsorted fields: F12, F7, F23
1598        let input = "F50={F12=1;F7=0;F23=test}";
1599        let mut parser = Parser::new(input).unwrap();
1600        let record = parser.parse_record().unwrap();
1601
1602        let field = record.get_field(50).unwrap();
1603        match &field.value {
1604            LnmpValue::NestedRecord(nested) => {
1605                // Fields should be sorted: F7, F12, F23
1606                assert_eq!(nested.fields()[0].fid, 7);
1607                assert_eq!(nested.fields()[1].fid, 12);
1608                assert_eq!(nested.fields()[2].fid, 23);
1609            }
1610            _ => panic!("Expected NestedRecord"),
1611        }
1612    }
1613
1614    #[test]
1615    fn test_parse_nested_record_with_trailing_semicolon() {
1616        let input = "F50={F12=1;F7=1;}";
1617        let mut parser = Parser::new(input).unwrap();
1618        let record = parser.parse_record().unwrap();
1619
1620        assert_eq!(record.fields().len(), 1);
1621        let field = record.get_field(50).unwrap();
1622
1623        match &field.value {
1624            LnmpValue::NestedRecord(nested) => {
1625                assert_eq!(nested.fields().len(), 2);
1626            }
1627            _ => panic!("Expected NestedRecord"),
1628        }
1629    }
1630
1631    #[test]
1632    fn test_parse_nested_array_basic() {
1633        let input = "F60=[{F12=1},{F12=2},{F12=3}]";
1634        let mut parser = Parser::new(input).unwrap();
1635        let record = parser.parse_record().unwrap();
1636
1637        assert_eq!(record.fields().len(), 1);
1638        let field = record.get_field(60).unwrap();
1639
1640        match &field.value {
1641            LnmpValue::NestedArray(records) => {
1642                assert_eq!(records.len(), 3);
1643                assert_eq!(
1644                    records[0].get_field(12).unwrap().value,
1645                    LnmpValue::Bool(true)
1646                );
1647                assert_eq!(records[1].get_field(12).unwrap().value, LnmpValue::Int(2));
1648                assert_eq!(records[2].get_field(12).unwrap().value, LnmpValue::Int(3));
1649            }
1650            _ => panic!("Expected NestedArray"),
1651        }
1652    }
1653
1654    #[test]
1655    fn test_parse_nested_array_with_multiple_fields() {
1656        let input = "F200=[{F1=alice;F2=admin},{F1=bob;F2=user}]";
1657        let mut parser = Parser::new(input).unwrap();
1658        let record = parser.parse_record().unwrap();
1659
1660        assert_eq!(record.fields().len(), 1);
1661        let field = record.get_field(200).unwrap();
1662
1663        match &field.value {
1664            LnmpValue::NestedArray(records) => {
1665                assert_eq!(records.len(), 2);
1666
1667                // First record
1668                assert_eq!(
1669                    records[0].get_field(1).unwrap().value,
1670                    LnmpValue::String("alice".to_string())
1671                );
1672                assert_eq!(
1673                    records[0].get_field(2).unwrap().value,
1674                    LnmpValue::String("admin".to_string())
1675                );
1676
1677                // Second record
1678                assert_eq!(
1679                    records[1].get_field(1).unwrap().value,
1680                    LnmpValue::String("bob".to_string())
1681                );
1682                assert_eq!(
1683                    records[1].get_field(2).unwrap().value,
1684                    LnmpValue::String("user".to_string())
1685                );
1686            }
1687            _ => panic!("Expected NestedArray"),
1688        }
1689    }
1690
1691    #[test]
1692    fn test_parse_empty_nested_array() {
1693        let input = "F60=[]";
1694        let mut parser = Parser::new(input).unwrap();
1695        let record = parser.parse_record().unwrap();
1696
1697        assert_eq!(record.fields().len(), 1);
1698        let field = record.get_field(60).unwrap();
1699
1700        // Empty array defaults to StringArray
1701        match &field.value {
1702            LnmpValue::StringArray(items) => {
1703                assert_eq!(items.len(), 0);
1704            }
1705            _ => panic!("Expected StringArray for empty array"),
1706        }
1707    }
1708
1709    #[test]
1710    fn test_parse_empty_nested_array_with_type_hint() {
1711        let input = "F60:ra=[]";
1712        let mut parser = Parser::new(input).unwrap();
1713        let record = parser.parse_record().unwrap();
1714
1715        assert_eq!(record.fields().len(), 1);
1716        let field = record.get_field(60).unwrap();
1717
1718        // With :ra type hint, empty array should be NestedArray
1719        match &field.value {
1720            LnmpValue::NestedArray(records) => {
1721                assert_eq!(records.len(), 0);
1722            }
1723            _ => panic!("Expected NestedArray for empty array with :ra type hint"),
1724        }
1725    }
1726
1727    #[test]
1728    fn test_parse_nested_record_with_type_hints() {
1729        let input = "F50:r={F12:i=14532;F7:b=1}";
1730        let mut parser = Parser::new(input).unwrap();
1731        let record = parser.parse_record().unwrap();
1732
1733        assert_eq!(record.fields().len(), 1);
1734        let field = record.get_field(50).unwrap();
1735
1736        match &field.value {
1737            LnmpValue::NestedRecord(nested) => {
1738                assert_eq!(nested.fields().len(), 2);
1739                assert_eq!(nested.get_field(7).unwrap().value, LnmpValue::Bool(true));
1740                assert_eq!(nested.get_field(12).unwrap().value, LnmpValue::Int(14532));
1741            }
1742            _ => panic!("Expected NestedRecord"),
1743        }
1744    }
1745
1746    #[test]
1747    fn test_parse_nested_array_with_type_hint() {
1748        let input = "F60:ra=[{F12=1},{F12=2}]";
1749        let mut parser = Parser::new(input).unwrap();
1750        let record = parser.parse_record().unwrap();
1751
1752        assert_eq!(record.fields().len(), 1);
1753        let field = record.get_field(60).unwrap();
1754
1755        match &field.value {
1756            LnmpValue::NestedArray(records) => {
1757                assert_eq!(records.len(), 2);
1758            }
1759            _ => panic!("Expected NestedArray"),
1760        }
1761    }
1762
1763    #[test]
1764    fn test_parse_nested_array_preserves_order() {
1765        // Requirement 5.4: Preserve element order in nested arrays
1766        let input = "F60=[{F1=first},{F1=second},{F1=third}]";
1767        let mut parser = Parser::new(input).unwrap();
1768        let record = parser.parse_record().unwrap();
1769
1770        let field = record.get_field(60).unwrap();
1771        match &field.value {
1772            LnmpValue::NestedArray(records) => {
1773                assert_eq!(records.len(), 3);
1774                assert_eq!(
1775                    records[0].get_field(1).unwrap().value,
1776                    LnmpValue::String("first".to_string())
1777                );
1778                assert_eq!(
1779                    records[1].get_field(1).unwrap().value,
1780                    LnmpValue::String("second".to_string())
1781                );
1782                assert_eq!(
1783                    records[2].get_field(1).unwrap().value,
1784                    LnmpValue::String("third".to_string())
1785                );
1786            }
1787            _ => panic!("Expected NestedArray"),
1788        }
1789    }
1790
1791    #[test]
1792    fn test_parse_nested_array_with_complex_records() {
1793        // Test nested arrays with records containing multiple field types
1794        let input = r#"F100=[{F1=alice;F2=30;F3=1},{F1=bob;F2=25;F3=0}]"#;
1795        let mut parser = Parser::new(input).unwrap();
1796        let record = parser.parse_record().unwrap();
1797
1798        let field = record.get_field(100).unwrap();
1799        match &field.value {
1800            LnmpValue::NestedArray(records) => {
1801                assert_eq!(records.len(), 2);
1802
1803                // First record
1804                let rec1 = &records[0];
1805                assert_eq!(
1806                    rec1.get_field(1).unwrap().value,
1807                    LnmpValue::String("alice".to_string())
1808                );
1809                assert_eq!(rec1.get_field(2).unwrap().value, LnmpValue::Int(30));
1810                assert_eq!(rec1.get_field(3).unwrap().value, LnmpValue::Bool(true));
1811
1812                // Second record
1813                let rec2 = &records[1];
1814                assert_eq!(
1815                    rec2.get_field(1).unwrap().value,
1816                    LnmpValue::String("bob".to_string())
1817                );
1818                assert_eq!(rec2.get_field(2).unwrap().value, LnmpValue::Int(25));
1819                assert_eq!(rec2.get_field(3).unwrap().value, LnmpValue::Bool(false));
1820            }
1821            _ => panic!("Expected NestedArray"),
1822        }
1823    }
1824
1825    #[test]
1826    fn test_parse_nested_array_single_element() {
1827        let input = "F60=[{F1=only}]";
1828        let mut parser = Parser::new(input).unwrap();
1829        let record = parser.parse_record().unwrap();
1830
1831        let field = record.get_field(60).unwrap();
1832        match &field.value {
1833            LnmpValue::NestedArray(records) => {
1834                assert_eq!(records.len(), 1);
1835                assert_eq!(
1836                    records[0].get_field(1).unwrap().value,
1837                    LnmpValue::String("only".to_string())
1838                );
1839            }
1840            _ => panic!("Expected NestedArray"),
1841        }
1842    }
1843
1844    #[test]
1845    fn test_parse_nested_array_with_all_value_types() {
1846        // Requirement 5.4: Support arrays containing records with mixed value types
1847        let input = r#"F100=[{F1=42;F2=3.14;F3=1;F4=test;F5=["a","b"]}]"#;
1848        let mut parser = Parser::new(input).unwrap();
1849        let record = parser.parse_record().unwrap();
1850
1851        let field = record.get_field(100).unwrap();
1852        match &field.value {
1853            LnmpValue::NestedArray(records) => {
1854                assert_eq!(records.len(), 1);
1855                let rec = &records[0];
1856
1857                // Verify all different value types are supported
1858                assert_eq!(rec.get_field(1).unwrap().value, LnmpValue::Int(42));
1859                assert_eq!(rec.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1860                assert_eq!(rec.get_field(3).unwrap().value, LnmpValue::Bool(true));
1861                assert_eq!(
1862                    rec.get_field(4).unwrap().value,
1863                    LnmpValue::String("test".to_string())
1864                );
1865                assert_eq!(
1866                    rec.get_field(5).unwrap().value,
1867                    LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1868                );
1869            }
1870            _ => panic!("Expected NestedArray"),
1871        }
1872    }
1873
1874    #[test]
1875    fn test_parse_mixed_top_level_and_nested() {
1876        let input = "F1=42;F50={F12=1;F7=1};F100=test";
1877        let mut parser = Parser::new(input).unwrap();
1878        let record = parser.parse_record().unwrap();
1879
1880        assert_eq!(record.fields().len(), 3);
1881        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1882        assert_eq!(
1883            record.get_field(100).unwrap().value,
1884            LnmpValue::String("test".to_string())
1885        );
1886
1887        match &record.get_field(50).unwrap().value {
1888            LnmpValue::NestedRecord(nested) => {
1889                assert_eq!(nested.fields().len(), 2);
1890            }
1891            _ => panic!("Expected NestedRecord"),
1892        }
1893    }
1894
1895    #[test]
1896    fn test_parse_three_level_nesting() {
1897        let input = "F1={F2={F3={F4=deep}}}";
1898        let mut parser = Parser::new(input).unwrap();
1899        let record = parser.parse_record().unwrap();
1900
1901        assert_eq!(record.fields().len(), 1);
1902
1903        // Level 1
1904        match &record.get_field(1).unwrap().value {
1905            LnmpValue::NestedRecord(level1) => {
1906                // Level 2
1907                match &level1.get_field(2).unwrap().value {
1908                    LnmpValue::NestedRecord(level2) => {
1909                        // Level 3
1910                        match &level2.get_field(3).unwrap().value {
1911                            LnmpValue::NestedRecord(level3) => {
1912                                assert_eq!(
1913                                    level3.get_field(4).unwrap().value,
1914                                    LnmpValue::String("deep".to_string())
1915                                );
1916                            }
1917                            _ => panic!("Expected NestedRecord at level 3"),
1918                        }
1919                    }
1920                    _ => panic!("Expected NestedRecord at level 2"),
1921                }
1922            }
1923            _ => panic!("Expected NestedRecord at level 1"),
1924        }
1925    }
1926
1927    #[test]
1928    fn test_nesting_too_deep_error() {
1929        use crate::config::ParserConfig;
1930
1931        let input = "F1={F2={F3={F4={F5={F6={F7={F8={F9={F10={F11={F12=1}}}}}}}}}}}}";
1932        let config = ParserConfig {
1933            max_nesting_depth: Some(10),
1934            ..Default::default()
1935        };
1936        let mut parser = Parser::with_config(input, config).unwrap();
1937        let result = parser.parse_record();
1938        assert!(result.is_err());
1939        match result {
1940            Err(LnmpError::NestingTooDeep {
1941                max_depth,
1942                actual_depth,
1943                ..
1944            }) => {
1945                assert_eq!(max_depth, 10);
1946                assert!(actual_depth > max_depth);
1947            }
1948            _ => panic!("Expected NestingTooDeep error"),
1949        }
1950    }
1951
1952    #[test]
1953    fn test_structural_limits_rejects_field_count() {
1954        use crate::config::ParserConfig;
1955        use lnmp_core::StructuralLimits;
1956
1957        let input = "F1=1\nF2=2";
1958        let config = ParserConfig {
1959            structural_limits: Some(StructuralLimits {
1960                max_fields: 1,
1961                ..Default::default()
1962            }),
1963            ..Default::default()
1964        };
1965
1966        let mut parser = Parser::with_config(input, config).unwrap();
1967        let result = parser.parse_record();
1968        match result {
1969            Err(LnmpError::InvalidNestedStructure { reason, .. }) => {
1970                assert!(reason.contains("maximum field count exceeded"));
1971            }
1972            _ => panic!("Expected InvalidNestedStructure due to structural limits"),
1973        }
1974    }
1975
1976    // Checksum parsing tests
1977    #[test]
1978    fn test_parse_field_with_checksum_ignored() {
1979        use lnmp_core::checksum::SemanticChecksum;
1980
1981        // Compute correct checksum
1982        let value = LnmpValue::Int(14532);
1983        let checksum = SemanticChecksum::compute(12, None, &value);
1984        let checksum_str = SemanticChecksum::format(checksum);
1985
1986        let input = format!("F12=14532#{}", checksum_str);
1987
1988        // Parse without validation (default)
1989        let mut parser = Parser::new(&input).unwrap();
1990        let record = parser.parse_record().unwrap();
1991
1992        assert_eq!(record.fields().len(), 1);
1993        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1994    }
1995
1996    #[test]
1997    fn test_parse_field_with_valid_checksum() {
1998        use crate::config::ParserConfig;
1999        use lnmp_core::checksum::SemanticChecksum;
2000
2001        // Compute correct checksum
2002        let value = LnmpValue::Int(14532);
2003        let checksum = SemanticChecksum::compute(12, None, &value);
2004        let checksum_str = SemanticChecksum::format(checksum);
2005
2006        let input = format!("F12=14532#{}", checksum_str);
2007
2008        // Parse with validation enabled
2009        let config = ParserConfig {
2010            validate_checksums: true,
2011            ..Default::default()
2012        };
2013        let mut parser = Parser::with_config(&input, config).unwrap();
2014        let record = parser.parse_record().unwrap();
2015
2016        assert_eq!(record.fields().len(), 1);
2017        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2018    }
2019
2020    #[test]
2021    fn test_parse_field_with_invalid_checksum() {
2022        use crate::config::ParserConfig;
2023
2024        // Use wrong checksum
2025        let input = "F12=14532#DEADBEEF";
2026
2027        // Parse with validation enabled
2028        let config = ParserConfig {
2029            validate_checksums: true,
2030            ..Default::default()
2031        };
2032        let mut parser = Parser::with_config(input, config).unwrap();
2033        let result = parser.parse_record();
2034
2035        assert!(result.is_err());
2036        match result {
2037            Err(LnmpError::ChecksumMismatch { field_id, .. }) => {
2038                assert_eq!(field_id, 12);
2039            }
2040            _ => panic!("Expected ChecksumMismatch error"),
2041        }
2042    }
2043
2044    #[test]
2045    fn test_parse_field_with_checksum_and_type_hint() {
2046        use crate::config::ParserConfig;
2047        use lnmp_core::checksum::SemanticChecksum;
2048
2049        // Compute correct checksum with type hint
2050        let value = LnmpValue::Int(14532);
2051        let checksum = SemanticChecksum::compute(12, Some(TypeHint::Int), &value);
2052        let checksum_str = SemanticChecksum::format(checksum);
2053
2054        let input = format!("F12:i=14532#{}", checksum_str);
2055
2056        // Parse with validation enabled
2057        let config = ParserConfig {
2058            validate_checksums: true,
2059            ..Default::default()
2060        };
2061        let mut parser = Parser::with_config(&input, config).unwrap();
2062        let record = parser.parse_record().unwrap();
2063
2064        assert_eq!(record.fields().len(), 1);
2065        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2066    }
2067
2068    #[test]
2069    fn test_parse_applies_semantic_dictionary_equivalence() {
2070        use crate::config::ParserConfig;
2071
2072        let mut dict = lnmp_sfe::SemanticDictionary::new();
2073        dict.add_equivalence(23, "admin".to_string(), "administrator".to_string());
2074
2075        let config = ParserConfig {
2076            semantic_dictionary: Some(dict),
2077            ..Default::default()
2078        };
2079
2080        let mut parser = Parser::with_config("F23=[admin]", config).unwrap();
2081        let record = parser.parse_record().unwrap();
2082        match record.get_field(23).unwrap().value.clone() {
2083            LnmpValue::StringArray(vals) => {
2084                assert_eq!(vals, vec!["administrator".to_string()]);
2085            }
2086            other => panic!("unexpected value {:?}", other),
2087        }
2088    }
2089
2090    #[test]
2091    fn test_parse_multiple_fields_with_checksums() {
2092        use crate::config::ParserConfig;
2093        use lnmp_core::checksum::SemanticChecksum;
2094
2095        let value1 = LnmpValue::Int(14532);
2096        let checksum1 = SemanticChecksum::compute(12, None, &value1);
2097        let checksum_str1 = SemanticChecksum::format(checksum1);
2098
2099        let value2 = LnmpValue::Bool(true);
2100        let checksum2 = SemanticChecksum::compute(7, None, &value2);
2101        let checksum_str2 = SemanticChecksum::format(checksum2);
2102
2103        let input = format!("F12=14532#{}\nF7=1#{}", checksum_str1, checksum_str2);
2104
2105        // Parse with validation enabled
2106        let config = ParserConfig {
2107            validate_checksums: true,
2108            ..Default::default()
2109        };
2110        let mut parser = Parser::with_config(&input, config).unwrap();
2111        let record = parser.parse_record().unwrap();
2112
2113        assert_eq!(record.fields().len(), 2);
2114        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2115        assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
2116    }
2117
2118    #[test]
2119    fn test_parse_field_require_checksum_missing() {
2120        use crate::config::ParserConfig;
2121
2122        let input = "F12=14532";
2123
2124        // Parse with checksums required
2125        let config = ParserConfig {
2126            require_checksums: true,
2127            ..Default::default()
2128        };
2129        let mut parser = Parser::with_config(input, config).unwrap();
2130        let result = parser.parse_record();
2131
2132        assert!(result.is_err());
2133        match result {
2134            Err(LnmpError::ChecksumMismatch { field_id, .. }) => {
2135                assert_eq!(field_id, 12);
2136            }
2137            _ => panic!("Expected ChecksumMismatch error"),
2138        }
2139    }
2140
2141    #[test]
2142    fn test_parse_field_with_checksum_after_comment() {
2143        use crate::config::ParserConfig;
2144        use lnmp_core::checksum::SemanticChecksum;
2145
2146        let value = LnmpValue::Int(14532);
2147        let checksum = SemanticChecksum::compute(12, None, &value);
2148        let checksum_str = SemanticChecksum::format(checksum);
2149
2150        let input = format!("# Comment\nF12=14532#{}", checksum_str);
2151
2152        // Parse with validation enabled
2153        let config = ParserConfig {
2154            validate_checksums: true,
2155            ..Default::default()
2156        };
2157        let mut parser = Parser::with_config(&input, config).unwrap();
2158        let record = parser.parse_record().unwrap();
2159
2160        assert_eq!(record.fields().len(), 1);
2161        assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2162    }
2163
2164    #[test]
2165    fn test_round_trip_with_checksum() {
2166        use crate::config::{EncoderConfig, ParserConfig};
2167        use crate::encoder::Encoder;
2168
2169        let mut record = LnmpRecord::new();
2170        record.add_field(LnmpField {
2171            fid: 12,
2172            value: LnmpValue::Int(14532),
2173        });
2174
2175        // Encode with checksum
2176        let encoder_config = EncoderConfig {
2177            enable_checksums: true,
2178            ..Default::default()
2179        };
2180        let encoder = Encoder::with_config(encoder_config);
2181        let output = encoder.encode(&record);
2182
2183        // Parse with validation
2184        let parser_config = ParserConfig {
2185            validate_checksums: true,
2186            ..Default::default()
2187        };
2188        let mut parser = Parser::with_config(&output, parser_config).unwrap();
2189        let parsed = parser.parse_record().unwrap();
2190
2191        assert_eq!(
2192            record.get_field(12).unwrap().value,
2193            parsed.get_field(12).unwrap().value
2194        );
2195    }
2196}