Skip to main content

automapper_validation/expr/
parser.rs

1//! Recursive descent parser for condition expressions.
2//!
3//! Grammar (from lowest to highest precedence):
4//!
5//! ```text
6//! expression  = xor_expr
7//! xor_expr    = or_expr (XOR or_expr)*
8//! or_expr     = and_expr (OR and_expr)*
9//! and_expr    = not_expr ((AND | implicit) not_expr)*
10//! not_expr    = NOT not_expr | primary
11//! primary     = CONDITION_ID | '(' expression ')'
12//! ```
13//!
14//! Implicit AND: two adjacent condition references or a condition followed by
15//! `(` without an intervening operator are treated as AND.
16
17use std::collections::BTreeMap;
18
19use super::ast::ConditionExpr;
20use super::status::{status_lines, StatusBranch, StatusKind};
21use super::token::{strip_status_prefix, tokenize, SpannedToken, Token};
22use crate::error::ParseError;
23
24/// Parser for AHB condition expressions.
25pub struct ConditionParser;
26
27impl ConditionParser {
28    /// Parse an AHB status string into a condition expression.
29    ///
30    /// Returns `Ok(None)` if the input contains no condition references
31    /// (e.g., bare `"Muss"` or empty string).
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use automapper_validation::expr::ConditionParser;
37    /// use automapper_validation::expr::ConditionExpr;
38    ///
39    /// let expr = ConditionParser::parse("Muss [494]").unwrap().unwrap();
40    /// assert_eq!(expr, ConditionExpr::Ref(494));
41    /// ```
42    pub fn parse(input: &str) -> Result<Option<ConditionExpr>, ParseError> {
43        Self::parse_with_ub(input, &BTreeMap::new())
44    }
45
46    /// Parse an AHB status string, expanding UB condition references inline.
47    ///
48    /// When `[UB1]` is encountered and `ub_definitions` contains "UB1", the
49    /// corresponding pre-parsed expression is substituted. Unknown UB references
50    /// fall back to the normal `parse_condition_id` behavior.
51    pub fn parse_with_ub(
52        input: &str,
53        ub_definitions: &BTreeMap<String, ConditionExpr>,
54    ) -> Result<Option<ConditionExpr>, ParseError> {
55        let input = input.trim();
56        if input.is_empty() {
57            return Ok(None);
58        }
59
60        // AHB_Status can carry alternative branches on separate lines, each with its
61        // own status prefix, e.g.
62        //     Muss [315] ∧ [707]
63        //     Soll [8] ∧ [301] ∧ [707]
64        // As one expression the branches form a disjunction: the part applies if
65        // any branch's condition holds. Which status then applies is
66        // `ConditionExprEvaluator::resolve_status`'s business.
67        let alternatives: Vec<ConditionExpr> = Self::parse_status_branches(input, ub_definitions)?
68            .into_iter()
69            .filter_map(|b| b.condition)
70            .collect();
71
72        match alternatives.len() {
73            0 => Ok(None),
74            1 => Ok(alternatives.into_iter().next()),
75            _ => Ok(Some(ConditionExpr::Or(alternatives))),
76        }
77    }
78
79    /// Parse an AHB status into one [`StatusBranch`] per status line.
80    ///
81    /// Validation asks for the same few hundred statuses of a format version
82    /// for every field of every group instance (an MSCONS Lastgang: thousands
83    /// of times each), so each thread keeps what it parsed. A status naming a
84    /// UB condition depends on `ub_definitions` as well and is not kept.
85    pub fn parse_status_branches(
86        input: &str,
87        ub_definitions: &BTreeMap<String, ConditionExpr>,
88    ) -> Result<Vec<StatusBranch>, ParseError> {
89        thread_local! {
90            static PARSED: std::cell::RefCell<
91                std::collections::HashMap<String, Result<Vec<StatusBranch>, ParseError>>,
92            > = Default::default();
93        }
94        // More distinct statuses than the format versions hold together
95        // means the input is not an AHB's; stop keeping them.
96        const KEPT: usize = 16_384;
97        if input.contains("UB") {
98            return Self::parse_status_branches_uncached(input, ub_definitions);
99        }
100        if let Some(hit) = PARSED.with(|p| p.borrow().get(input).cloned()) {
101            return hit;
102        }
103        let parsed = Self::parse_status_branches_uncached(input, ub_definitions);
104        PARSED.with(|p| {
105            let mut p = p.borrow_mut();
106            if p.len() < KEPT {
107                p.insert(input.to_string(), parsed.clone());
108            }
109        });
110        parsed
111    }
112
113    fn parse_status_branches_uncached(
114        input: &str,
115        ub_definitions: &BTreeMap<String, ConditionExpr>,
116    ) -> Result<Vec<StatusBranch>, ParseError> {
117        let mut branches = Vec::new();
118        for line in status_lines(input) {
119            let kind = StatusKind::of(line);
120            let stripped = strip_status_prefix(line);
121            // A bare status word ("Kann") is left as it is: no condition.
122            let condition = if kind.is_some() && stripped == line {
123                None
124            } else {
125                let tokens = tokenize(stripped)?;
126                let mut pos = 0;
127                parse_expression(&tokens, &mut pos, ub_definitions)?
128            };
129            branches.push(StatusBranch { kind, condition });
130        }
131        Ok(branches)
132    }
133
134    /// Parse an expression that is known to contain conditions (no prefix stripping).
135    ///
136    /// Returns `Err` if the input cannot be parsed. Returns `Ok(None)` if empty.
137    pub fn parse_raw(input: &str) -> Result<Option<ConditionExpr>, ParseError> {
138        let input = input.trim();
139        if input.is_empty() {
140            return Ok(None);
141        }
142
143        let tokens = tokenize(input)?;
144        if tokens.is_empty() {
145            return Ok(None);
146        }
147
148        let mut pos = 0;
149        let expr = parse_expression(&tokens, &mut pos, &BTreeMap::new())?;
150
151        Ok(expr)
152    }
153}
154
155/// Parse a full expression (entry point for precedence climbing).
156fn parse_expression(
157    tokens: &[SpannedToken],
158    pos: &mut usize,
159    ub_definitions: &BTreeMap<String, ConditionExpr>,
160) -> Result<Option<ConditionExpr>, ParseError> {
161    parse_xor(tokens, pos, ub_definitions)
162}
163
164/// XOR has the lowest precedence.
165fn parse_xor(
166    tokens: &[SpannedToken],
167    pos: &mut usize,
168    ub_definitions: &BTreeMap<String, ConditionExpr>,
169) -> Result<Option<ConditionExpr>, ParseError> {
170    let mut left = match parse_or(tokens, pos, ub_definitions)? {
171        Some(expr) => expr,
172        None => return Ok(None),
173    };
174
175    while *pos < tokens.len() && tokens[*pos].token == Token::Xor {
176        *pos += 1; // consume XOR
177        let right = match parse_or(tokens, pos, ub_definitions)? {
178            Some(expr) => expr,
179            None => return Ok(Some(left)),
180        };
181        left = ConditionExpr::Xor(Box::new(left), Box::new(right));
182    }
183
184    Ok(Some(left))
185}
186
187/// OR has middle-low precedence.
188fn parse_or(
189    tokens: &[SpannedToken],
190    pos: &mut usize,
191    ub_definitions: &BTreeMap<String, ConditionExpr>,
192) -> Result<Option<ConditionExpr>, ParseError> {
193    let mut left = match parse_and(tokens, pos, ub_definitions)? {
194        Some(expr) => expr,
195        None => return Ok(None),
196    };
197
198    while *pos < tokens.len() && tokens[*pos].token == Token::Or {
199        *pos += 1; // consume OR
200        let right = match parse_and(tokens, pos, ub_definitions)? {
201            Some(expr) => expr,
202            None => return Ok(Some(left)),
203        };
204        // Flatten nested ORs into a single Or(vec![...])
205        left = match left {
206            ConditionExpr::Or(mut exprs) => {
207                exprs.push(right);
208                ConditionExpr::Or(exprs)
209            }
210            _ => ConditionExpr::Or(vec![left, right]),
211        };
212    }
213
214    Ok(Some(left))
215}
216
217/// AND has middle-high precedence. Also handles implicit AND between adjacent
218/// conditions or parenthesized groups.
219fn parse_and(
220    tokens: &[SpannedToken],
221    pos: &mut usize,
222    ub_definitions: &BTreeMap<String, ConditionExpr>,
223) -> Result<Option<ConditionExpr>, ParseError> {
224    let mut left = match parse_not(tokens, pos, ub_definitions)? {
225        Some(expr) => expr,
226        None => return Ok(None),
227    };
228
229    while *pos < tokens.len() {
230        if tokens[*pos].token == Token::And {
231            *pos += 1; // consume explicit AND
232            let right = match parse_not(tokens, pos, ub_definitions)? {
233                Some(expr) => expr,
234                None => return Ok(Some(left)),
235            };
236            left = flatten_and(left, right);
237        } else if matches!(
238            tokens[*pos].token,
239            Token::ConditionId(_) | Token::LeftParen | Token::Not
240        ) {
241            // Implicit AND: adjacent condition, paren, or NOT without operator
242            let right = match parse_not(tokens, pos, ub_definitions)? {
243                Some(expr) => expr,
244                None => return Ok(Some(left)),
245            };
246            left = flatten_and(left, right);
247        } else {
248            break;
249        }
250    }
251
252    Ok(Some(left))
253}
254
255/// Flatten nested ANDs into a single And(vec![...]).
256fn flatten_and(left: ConditionExpr, right: ConditionExpr) -> ConditionExpr {
257    match left {
258        ConditionExpr::And(mut exprs) => {
259            exprs.push(right);
260            ConditionExpr::And(exprs)
261        }
262        _ => ConditionExpr::And(vec![left, right]),
263    }
264}
265
266/// NOT has the highest precedence (unary prefix).
267fn parse_not(
268    tokens: &[SpannedToken],
269    pos: &mut usize,
270    ub_definitions: &BTreeMap<String, ConditionExpr>,
271) -> Result<Option<ConditionExpr>, ParseError> {
272    if *pos < tokens.len() && tokens[*pos].token == Token::Not {
273        *pos += 1; // consume NOT
274        let inner = match parse_not(tokens, pos, ub_definitions)? {
275            Some(expr) => expr,
276            None => {
277                return Err(ParseError::UnexpectedToken {
278                    position: if *pos < tokens.len() {
279                        tokens[*pos].position
280                    } else {
281                        0
282                    },
283                    expected: "expression after NOT".to_string(),
284                    found: "end of input".to_string(),
285                });
286            }
287        };
288        return Ok(Some(ConditionExpr::Not(Box::new(inner))));
289    }
290    parse_primary(tokens, pos, ub_definitions)
291}
292
293/// Primary: a condition reference or a parenthesized expression.
294///
295/// When a `ConditionId` matches a key in `ub_definitions`, the pre-parsed
296/// UB expression is substituted inline instead of calling `parse_condition_id`.
297fn parse_primary(
298    tokens: &[SpannedToken],
299    pos: &mut usize,
300    ub_definitions: &BTreeMap<String, ConditionExpr>,
301) -> Result<Option<ConditionExpr>, ParseError> {
302    if *pos >= tokens.len() {
303        return Ok(None);
304    }
305
306    match &tokens[*pos].token {
307        Token::ConditionId(id) => {
308            *pos += 1;
309            // Check for UB expansion before falling back to parse_condition_id
310            if let Some(ub_expr) = ub_definitions.get(id.as_str()) {
311                return Ok(Some(ub_expr.clone()));
312            }
313            Ok(Some(parse_condition_id(id)))
314        }
315        Token::LeftParen => {
316            *pos += 1; // consume (
317            let expr = parse_expression(tokens, pos, ub_definitions)?;
318            // Consume closing paren if present (graceful handling of missing)
319            if *pos < tokens.len() && tokens[*pos].token == Token::RightParen {
320                *pos += 1;
321            }
322            Ok(expr)
323        }
324        _ => Ok(None),
325    }
326}
327
328/// Parse a condition ID string into a ConditionExpr.
329///
330/// Numeric IDs become `Ref(n)`. Non-numeric IDs (like `UB1`, `10P1..5`)
331/// are kept as-is by extracting numeric portions. For the Rust port,
332/// pure numeric IDs use `Ref(u32)`. Non-numeric IDs extract leading
333/// digits if present, otherwise use 0 as a sentinel.
334fn parse_condition_id(id: &str) -> ConditionExpr {
335    // Try pure numeric ID first
336    if let Ok(num) = id.parse::<u32>() {
337        return ConditionExpr::Ref(num);
338    }
339
340    // Check for package condition: NP or NPmin..max
341    if let Some(p_pos) = id.find('P') {
342        let num_part = &id[..p_pos];
343        let range_part = &id[p_pos + 1..];
344        if let Ok(pkg_id) = num_part.parse::<u32>() {
345            let (min, max) = parse_package_range(range_part);
346            return ConditionExpr::Package {
347                id: pkg_id,
348                min,
349                max,
350            };
351        }
352    }
353
354    // For non-numeric, non-package IDs (e.g., "UB1"), extract leading digits
355    let numeric_part: String = id.chars().take_while(|c| c.is_ascii_digit()).collect();
356    if let Ok(num) = numeric_part.parse::<u32>() {
357        ConditionExpr::Ref(num)
358    } else {
359        ConditionExpr::Ref(0)
360    }
361}
362
363/// Parse the range part of a package condition: `"0..1"` → `(0, 1)`, `""` → `(0, MAX)`.
364fn parse_package_range(range: &str) -> (u32, u32) {
365    if range.is_empty() {
366        return (0, u32::MAX);
367    }
368    if let Some((min_str, max_str)) = range.split_once("..") {
369        let min = min_str.parse::<u32>().unwrap_or(0);
370        let max = max_str.parse::<u32>().unwrap_or(u32::MAX);
371        (min, max)
372    } else {
373        let n = range.parse::<u32>().unwrap_or(0);
374        (n, n)
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use pretty_assertions::assert_eq;
382
383    // === Basic parsing ===
384
385    #[test]
386    fn test_parse_single_condition() {
387        let result = ConditionParser::parse("[931]").unwrap().unwrap();
388        assert_eq!(result, ConditionExpr::Ref(931));
389    }
390
391    #[test]
392    fn test_parse_with_muss_prefix() {
393        let result = ConditionParser::parse("Muss [494]").unwrap().unwrap();
394        assert_eq!(result, ConditionExpr::Ref(494));
395    }
396
397    #[test]
398    fn test_parse_with_soll_prefix() {
399        let result = ConditionParser::parse("Soll [494]").unwrap().unwrap();
400        assert_eq!(result, ConditionExpr::Ref(494));
401    }
402
403    #[test]
404    fn test_parse_with_kann_prefix() {
405        let result = ConditionParser::parse("Kann [182]").unwrap().unwrap();
406        assert_eq!(result, ConditionExpr::Ref(182));
407    }
408
409    #[test]
410    fn test_parse_with_x_prefix() {
411        let result = ConditionParser::parse("X [567]").unwrap().unwrap();
412        assert_eq!(result, ConditionExpr::Ref(567));
413    }
414
415    // === Binary operators ===
416
417    #[test]
418    fn test_parse_simple_and() {
419        let result = ConditionParser::parse("[182] ∧ [152]").unwrap().unwrap();
420        assert_eq!(
421            result,
422            ConditionExpr::And(vec![ConditionExpr::Ref(182), ConditionExpr::Ref(152)])
423        );
424    }
425
426    #[test]
427    fn test_parse_simple_or() {
428        let result = ConditionParser::parse("[1] ∨ [2]").unwrap().unwrap();
429        assert_eq!(
430            result,
431            ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
432        );
433    }
434
435    #[test]
436    fn test_parse_simple_xor() {
437        let result = ConditionParser::parse("[1] ⊻ [2]").unwrap().unwrap();
438        assert_eq!(
439            result,
440            ConditionExpr::Xor(
441                Box::new(ConditionExpr::Ref(1)),
442                Box::new(ConditionExpr::Ref(2)),
443            )
444        );
445    }
446
447    // === Chained operators ===
448
449    #[test]
450    fn test_parse_three_way_and() {
451        let result = ConditionParser::parse("[1] ∧ [2] ∧ [3]").unwrap().unwrap();
452        assert_eq!(
453            result,
454            ConditionExpr::And(vec![
455                ConditionExpr::Ref(1),
456                ConditionExpr::Ref(2),
457                ConditionExpr::Ref(3),
458            ])
459        );
460    }
461
462    #[test]
463    fn test_parse_three_way_and_with_prefix() {
464        let result = ConditionParser::parse("Kann [182] ∧ [6] ∧ [570]")
465            .unwrap()
466            .unwrap();
467        assert_eq!(
468            result,
469            ConditionExpr::And(vec![
470                ConditionExpr::Ref(182),
471                ConditionExpr::Ref(6),
472                ConditionExpr::Ref(570),
473            ])
474        );
475        assert_eq!(result.condition_ids(), [6, 182, 570].into());
476    }
477
478    #[test]
479    fn test_parse_multiple_xor() {
480        let result = ConditionParser::parse("[1] ⊻ [2] ⊻ [3] ⊻ [4]")
481            .unwrap()
482            .unwrap();
483        assert_eq!(result.condition_ids(), [1, 2, 3, 4].into());
484    }
485
486    // === Parentheses ===
487
488    #[test]
489    fn test_parse_parenthesized_expression() {
490        let result = ConditionParser::parse("([1] ∨ [2]) ∧ [3]")
491            .unwrap()
492            .unwrap();
493        assert_eq!(
494            result,
495            ConditionExpr::And(vec![
496                ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
497                ConditionExpr::Ref(3),
498            ])
499        );
500    }
501
502    #[test]
503    fn test_parse_nested_parentheses() {
504        // (([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]
505        let result = ConditionParser::parse("(([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]")
506            .unwrap()
507            .unwrap();
508        assert_eq!(result.condition_ids(), [1, 2, 3, 4, 5].into());
509        // Outer is AND
510        match &result {
511            ConditionExpr::And(exprs) => {
512                assert_eq!(exprs.len(), 2);
513                assert!(matches!(&exprs[0], ConditionExpr::Or(_)));
514                assert_eq!(exprs[1], ConditionExpr::Ref(5));
515            }
516            other => panic!("Expected And, got {other:?}"),
517        }
518    }
519
520    // === Operator precedence ===
521
522    #[test]
523    fn test_and_has_higher_precedence_than_or() {
524        // [1] ∨ [2] ∧ [3] should parse as [1] ∨ ([2] ∧ [3])
525        let result = ConditionParser::parse("[1] ∨ [2] ∧ [3]").unwrap().unwrap();
526        assert_eq!(
527            result,
528            ConditionExpr::Or(vec![
529                ConditionExpr::Ref(1),
530                ConditionExpr::And(vec![ConditionExpr::Ref(2), ConditionExpr::Ref(3)]),
531            ])
532        );
533    }
534
535    #[test]
536    fn test_or_has_higher_precedence_than_xor() {
537        // [1] ⊻ [2] ∨ [3] should parse as [1] ⊻ ([2] ∨ [3])
538        let result = ConditionParser::parse("[1] ⊻ [2] ∨ [3]").unwrap().unwrap();
539        assert_eq!(
540            result,
541            ConditionExpr::Xor(
542                Box::new(ConditionExpr::Ref(1)),
543                Box::new(ConditionExpr::Or(vec![
544                    ConditionExpr::Ref(2),
545                    ConditionExpr::Ref(3),
546                ])),
547            )
548        );
549    }
550
551    // === Implicit AND ===
552
553    #[test]
554    fn test_adjacent_conditions_implicit_and() {
555        // "[1] [2]" is equivalent to "[1] ∧ [2]"
556        let result = ConditionParser::parse("[1] [2]").unwrap().unwrap();
557        assert_eq!(
558            result,
559            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
560        );
561    }
562
563    #[test]
564    fn test_adjacent_conditions_no_space_implicit_and() {
565        // "[939][14]" from real AHB XML
566        let result = ConditionParser::parse("[939][14]").unwrap().unwrap();
567        assert_eq!(
568            result,
569            ConditionExpr::And(vec![ConditionExpr::Ref(939), ConditionExpr::Ref(14)])
570        );
571    }
572
573    // === NOT operator ===
574
575    #[test]
576    fn test_parse_not() {
577        let result = ConditionParser::parse("NOT [1]").unwrap().unwrap();
578        assert_eq!(result, ConditionExpr::Not(Box::new(ConditionExpr::Ref(1))));
579    }
580
581    #[test]
582    fn test_parse_not_with_and() {
583        // NOT [1] ∧ [2] should parse as (NOT [1]) ∧ [2] because NOT has highest precedence
584        let result = ConditionParser::parse("NOT [1] ∧ [2]").unwrap().unwrap();
585        assert_eq!(
586            result,
587            ConditionExpr::And(vec![
588                ConditionExpr::Not(Box::new(ConditionExpr::Ref(1))),
589                ConditionExpr::Ref(2),
590            ])
591        );
592    }
593
594    // === Real-world AHB expressions ===
595
596    #[test]
597    fn test_real_world_orders_expression() {
598        // From ORDERS AHB: "X (([939] [147]) ∨ ([940] [148])) ∧ [567]"
599        let result = ConditionParser::parse("X (([939] [147]) ∨ ([940] [148])) ∧ [567]")
600            .unwrap()
601            .unwrap();
602        assert_eq!(result.condition_ids(), [147, 148, 567, 939, 940].into());
603    }
604
605    #[test]
606    fn test_real_world_xor_expression() {
607        // "Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])"
608        let result = ConditionParser::parse("Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])")
609            .unwrap()
610            .unwrap();
611        assert!(matches!(result, ConditionExpr::Xor(_, _)));
612        assert_eq!(result.condition_ids(), [102, 103, 2005, 2006].into());
613    }
614
615    #[test]
616    fn test_real_world_complex_nested_with_implicit_and() {
617        // "([939][14]) ∨ ([940][15])"
618        let result = ConditionParser::parse("([939][14]) ∨ ([940][15])")
619            .unwrap()
620            .unwrap();
621        assert!(matches!(result, ConditionExpr::Or(_)));
622        assert_eq!(result.condition_ids(), [14, 15, 939, 940].into());
623    }
624
625    // === Edge cases ===
626
627    #[test]
628    fn test_parse_empty_string() {
629        assert!(ConditionParser::parse("").unwrap().is_none());
630    }
631
632    #[test]
633    fn test_parse_whitespace_only() {
634        assert!(ConditionParser::parse("   \t  ").unwrap().is_none());
635    }
636
637    #[test]
638    fn test_parse_bare_muss() {
639        assert!(ConditionParser::parse("Muss").unwrap().is_none());
640    }
641
642    #[test]
643    fn test_parse_bare_x() {
644        // "X" alone has no conditions after it
645        assert!(ConditionParser::parse("X").unwrap().is_none());
646    }
647
648    #[test]
649    fn test_parse_unmatched_open_paren_graceful() {
650        // ([1] ∧ [2] — missing closing paren
651        let result = ConditionParser::parse("([1] ∧ [2]").unwrap().unwrap();
652        assert_eq!(
653            result,
654            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
655        );
656    }
657
658    #[test]
659    fn test_parse_text_and_operator() {
660        let result = ConditionParser::parse("[1] AND [2]").unwrap().unwrap();
661        assert_eq!(
662            result,
663            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
664        );
665    }
666
667    #[test]
668    fn test_parse_text_or_operator() {
669        let result = ConditionParser::parse("[1] OR [2]").unwrap().unwrap();
670        assert_eq!(
671            result,
672            ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
673        );
674    }
675
676    #[test]
677    fn test_parse_text_xor_operator() {
678        let result = ConditionParser::parse("[1] XOR [2]").unwrap().unwrap();
679        assert_eq!(
680            result,
681            ConditionExpr::Xor(
682                Box::new(ConditionExpr::Ref(1)),
683                Box::new(ConditionExpr::Ref(2)),
684            )
685        );
686    }
687
688    #[test]
689    fn test_parse_mixed_unicode_and_text_operators() {
690        let result = ConditionParser::parse("[1] ∧ [2] OR [3]").unwrap().unwrap();
691        assert_eq!(
692            result,
693            ConditionExpr::Or(vec![
694                ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
695                ConditionExpr::Ref(3),
696            ])
697        );
698    }
699
700    #[test]
701    fn test_parse_deeply_nested() {
702        // ((([1])))
703        let result = ConditionParser::parse("((([1])))").unwrap().unwrap();
704        assert_eq!(result, ConditionExpr::Ref(1));
705    }
706
707    // === Package conditions ===
708
709    #[test]
710    fn test_parse_package_condition_0_1() {
711        let result = ConditionParser::parse("[4P0..1]").unwrap().unwrap();
712        assert_eq!(
713            result,
714            ConditionExpr::Package {
715                id: 4,
716                min: 0,
717                max: 1
718            }
719        );
720    }
721
722    #[test]
723    fn test_parse_package_condition_1_5() {
724        let result = ConditionParser::parse("[10P1..5]").unwrap().unwrap();
725        assert_eq!(
726            result,
727            ConditionExpr::Package {
728                id: 10,
729                min: 1,
730                max: 5
731            }
732        );
733    }
734
735    #[test]
736    fn test_parse_package_in_expression() {
737        let result = ConditionParser::parse("X [4P0..1] ⊻ [5P0..1]")
738            .unwrap()
739            .unwrap();
740        assert_eq!(
741            result,
742            ConditionExpr::Xor(
743                Box::new(ConditionExpr::Package {
744                    id: 4,
745                    min: 0,
746                    max: 1
747                }),
748                Box::new(ConditionExpr::Package {
749                    id: 5,
750                    min: 0,
751                    max: 1
752                }),
753            )
754        );
755    }
756
757    #[test]
758    fn test_parse_package_bare_p() {
759        let result = ConditionParser::parse("[1P]").unwrap().unwrap();
760        assert_eq!(
761            result,
762            ConditionExpr::Package {
763                id: 1,
764                min: 0,
765                max: u32::MAX
766            }
767        );
768    }
769
770    #[test]
771    fn test_condition_ids_extraction_full() {
772        let result = ConditionParser::parse("Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])")
773            .unwrap()
774            .unwrap();
775        let ids = result.condition_ids();
776        assert!(ids.contains(&102));
777        assert!(ids.contains(&103));
778        assert!(ids.contains(&2005));
779        assert!(ids.contains(&2006));
780        assert_eq!(ids.len(), 4);
781    }
782
783    // === UB inline expansion ===
784
785    #[test]
786    fn test_parse_ub_inline_expansion() {
787        let ub1_expr = ConditionParser::parse("[931] ∧ [932]").unwrap().unwrap();
788        let mut ub_map = BTreeMap::new();
789        ub_map.insert("UB1".to_string(), ub1_expr.clone());
790
791        let result = ConditionParser::parse_with_ub("X [UB1]", &ub_map)
792            .unwrap()
793            .unwrap();
794        assert_eq!(result, ub1_expr);
795    }
796
797    /// Statuses are kept per thread once parsed; one naming a UB condition
798    /// depends on the UB definitions it is parsed with and must follow them.
799    #[test]
800    fn a_kept_status_follows_its_ub_definitions() {
801        let with = |expr: &str| {
802            let mut ub_map = BTreeMap::new();
803            ub_map.insert(
804                "UB1".to_string(),
805                ConditionParser::parse(expr).unwrap().unwrap(),
806            );
807            ConditionParser::parse_with_ub("X [931] ∧ [UB1]", &ub_map)
808                .unwrap()
809                .unwrap()
810        };
811        let first = with("[1]");
812        assert_eq!(first, with("[1]"));
813        assert_ne!(first, with("[2]"));
814        // A status without one is the same however often it is asked for.
815        let plain = ConditionParser::parse("Muss [92] ∧ [126]").unwrap();
816        assert_eq!(plain, ConditionParser::parse("Muss [92] ∧ [126]").unwrap());
817    }
818
819    #[test]
820    fn test_parse_ub_unknown_falls_back() {
821        let ub_map = BTreeMap::new();
822        let result = ConditionParser::parse_with_ub("[UB99]", &ub_map)
823            .unwrap()
824            .unwrap();
825        assert_eq!(result, ConditionExpr::Ref(0));
826    }
827
828    #[test]
829    fn test_parse_with_ub_empty_map_same_as_parse() {
830        let ub_map = BTreeMap::new();
831        // Normal conditions should work unchanged
832        let result = ConditionParser::parse_with_ub("X [931] ∧ [932]", &ub_map)
833            .unwrap()
834            .unwrap();
835        let expected = ConditionParser::parse("X [931] ∧ [932]").unwrap().unwrap();
836        assert_eq!(result, expected);
837    }
838
839    #[test]
840    fn test_parse_ub_in_complex_expression() {
841        // UB expands inside a larger expression
842        let ub1_expr = ConditionParser::parse("[931] ∧ [932]").unwrap().unwrap();
843        let mut ub_map = BTreeMap::new();
844        ub_map.insert("UB1".to_string(), ub1_expr);
845
846        let result = ConditionParser::parse_with_ub("X [UB1] ∨ [100]", &ub_map)
847            .unwrap()
848            .unwrap();
849        // Should be: ([931] ∧ [932]) ∨ [100]
850        assert_eq!(
851            result,
852            ConditionExpr::Or(vec![
853                ConditionExpr::And(vec![ConditionExpr::Ref(931), ConditionExpr::Ref(932)]),
854                ConditionExpr::Ref(100),
855            ])
856        );
857    }
858
859    #[test]
860    fn test_parse_ub_multiple_references() {
861        // Two different UB references in one expression
862        let ub1_expr = ConditionParser::parse("[931]").unwrap().unwrap();
863        let ub2_expr = ConditionParser::parse("[932]").unwrap().unwrap();
864        let mut ub_map = BTreeMap::new();
865        ub_map.insert("UB1".to_string(), ub1_expr);
866        ub_map.insert("UB2".to_string(), ub2_expr);
867
868        let result = ConditionParser::parse_with_ub("X [UB1] ∧ [UB2]", &ub_map)
869            .unwrap()
870            .unwrap();
871        assert_eq!(
872            result,
873            ConditionExpr::And(vec![ConditionExpr::Ref(931), ConditionExpr::Ref(932)])
874        );
875    }
876
877    #[test]
878    fn test_parse_multi_line_alternatives_are_or() {
879        // AHB_Status with alternative branches on separate lines, each with its
880        // own status prefix. Branches form a disjunction (at least one must hold).
881        let status = "Muss [315] ∧ [707]\nSoll [8] ∧ [301] ∧ [707]";
882        let result = ConditionParser::parse(status).unwrap().unwrap();
883        assert_eq!(
884            result,
885            ConditionExpr::Or(vec![
886                ConditionExpr::And(vec![ConditionExpr::Ref(315), ConditionExpr::Ref(707)]),
887                ConditionExpr::And(vec![
888                    ConditionExpr::Ref(8),
889                    ConditionExpr::Ref(301),
890                    ConditionExpr::Ref(707),
891                ]),
892            ])
893        );
894    }
895
896    #[test]
897    fn test_parse_multi_line_with_crlf() {
898        // XML parsing produces \r\n line endings for multi-line AHB_Status values.
899        let result = ConditionParser::parse("Muss [10]\r\nSoll [20]")
900            .unwrap()
901            .unwrap();
902        assert_eq!(
903            result,
904            ConditionExpr::Or(vec![ConditionExpr::Ref(10), ConditionExpr::Ref(20)])
905        );
906    }
907
908    #[test]
909    fn test_parse_single_line_unchanged() {
910        // Single-line inputs continue to produce a bare expression (not wrapped in Or).
911        let result = ConditionParser::parse("Muss [315] ∧ [707]")
912            .unwrap()
913            .unwrap();
914        assert_eq!(
915            result,
916            ConditionExpr::And(vec![ConditionExpr::Ref(315), ConditionExpr::Ref(707)])
917        );
918    }
919
920    // --- parse_status_branches: one branch per status line ---
921
922    fn branches(status: &str) -> Vec<StatusBranch> {
923        ConditionParser::parse_status_branches(status, &BTreeMap::new()).unwrap()
924    }
925
926    #[test]
927    fn test_status_branches_keep_each_lines_status() {
928        assert_eq!(
929            branches("Muss [2119] \r\nSoll [130]"),
930            vec![
931                StatusBranch {
932                    kind: Some(StatusKind::Muss),
933                    condition: Some(ConditionExpr::Ref(2119)),
934                },
935                StatusBranch {
936                    kind: Some(StatusKind::Soll),
937                    condition: Some(ConditionExpr::Ref(130)),
938                },
939            ]
940        );
941    }
942
943    #[test]
944    fn test_status_branches_bare_line_has_no_condition() {
945        assert_eq!(
946            branches("Muss [48]\r\nKann"),
947            vec![
948                StatusBranch {
949                    kind: Some(StatusKind::Muss),
950                    condition: Some(ConditionExpr::Ref(48)),
951                },
952                StatusBranch {
953                    kind: Some(StatusKind::Kann),
954                    condition: None,
955                },
956            ]
957        );
958    }
959
960    #[test]
961    fn test_status_branches_x_is_muss() {
962        assert_eq!(
963            branches("X [931]"),
964            vec![StatusBranch {
965                kind: Some(StatusKind::Muss),
966                condition: Some(ConditionExpr::Ref(931)),
967            }]
968        );
969    }
970
971    #[test]
972    fn test_status_branches_split_a_second_status_on_the_same_line() {
973        // IFTSTA writes some two-status cells without a line break.
974        assert_eq!(
975            branches("Muss [56] ∧ [59] Soll [70]"),
976            vec![
977                StatusBranch {
978                    kind: Some(StatusKind::Muss),
979                    condition: Some(ConditionExpr::And(vec![
980                        ConditionExpr::Ref(56),
981                        ConditionExpr::Ref(59),
982                    ])),
983                },
984                StatusBranch {
985                    kind: Some(StatusKind::Soll),
986                    condition: Some(ConditionExpr::Ref(70)),
987                },
988            ]
989        );
990    }
991
992    #[test]
993    fn test_status_branches_do_not_split_inside_parentheses() {
994        // The PID schema joins nested group statuses into one parent status.
995        // IFTSTA 21038's SG18 carries a one-line two-status cell, which ends up
996        // parenthesised: splitting there would cut the parentheses apart.
997        let status = "Muss ([525]) ∧ ([56] ∧ [59] Soll [70]) ∧ ([54])";
998        let found = branches(status);
999        assert_eq!(found.len(), 1, "{found:?}");
1000        assert_eq!(found[0].kind, Some(StatusKind::Muss));
1001        assert_eq!(
1002            found[0].condition,
1003            ConditionParser::parse_raw("([525]) ∧ ([56] ∧ [59] [70]) ∧ ([54])").unwrap()
1004        );
1005    }
1006
1007    #[test]
1008    fn test_parse_same_line_second_status_is_or() {
1009        let result = ConditionParser::parse("Muss [56] ∧ [59] Soll [70]")
1010            .unwrap()
1011            .unwrap();
1012        assert_eq!(
1013            result,
1014            ConditionExpr::Or(vec![
1015                ConditionExpr::And(vec![ConditionExpr::Ref(56), ConditionExpr::Ref(59)]),
1016                ConditionExpr::Ref(70),
1017            ])
1018        );
1019    }
1020}