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