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