//! Shared expression parser/evaluator for `${{ }}` expressions.
//!
//! Used by both GitHub Actions (`${{ matrix.os }}`) and GitLab Moa
//! (`${{ inputs.name || "default" }}`).

/// Trait for resolving identifiers in expressions.
pub trait ExprContext {
    fn resolve(&self, identifier: &str) -> Option<String>;
}

#[derive(Debug)]
enum ExprToken {
    Str(String),
    DblStr(String),
    Ident(String),
    Number(f64),
    Eq,
    Ne,
    Lt,
    Le,
    Gt,
    Ge,
    And,
    Or,
    Not,
    Plus,
    Minus,
    Star,
    Slash,
    Percent,
    LParen,
    RParen,
    Comma,
}

fn tokenize_expression(expr: &str) -> Vec<ExprToken> {
    let mut tokens = Vec::new();
    let bytes = expr.as_bytes();
    let mut i = 0;

    while i < bytes.len() {
        match bytes[i] {
            b' ' | b'\t' | b'\n' | b'\r' => i += 1,
            b'\'' => {
                i += 1;
                let mut s = String::new();
                while i < bytes.len() && bytes[i] != b'\'' {
                    if bytes[i] == b'\\' && i + 1 < bytes.len() {
                        match bytes[i + 1] {
                            b'\\' => s.push('\\'),
                            b'\'' => s.push('\''),
                            _ => {
                                s.push('\\');
                                s.push_str(&expr[i + 1..i + 2]);
                            }
                        }
                        i += 2;
                    } else {
                        let ch = &expr[i..];
                        let c = ch.chars().next().unwrap();
                        s.push(c);
                        i += c.len_utf8();
                    }
                }
                tokens.push(ExprToken::Str(s));
                if i < bytes.len() {
                    i += 1;
                }
            }
            b'"' => {
                i += 1;
                let mut s = String::new();
                while i < bytes.len() && bytes[i] != b'"' {
                    if bytes[i] == b'\\' && i + 1 < bytes.len() {
                        match bytes[i + 1] {
                            b'n' => s.push('\n'),
                            b't' => s.push('\t'),
                            b'r' => s.push('\r'),
                            b'a' => s.push('\x07'),
                            b'b' => s.push('\x08'),
                            b'f' => s.push('\x0C'),
                            b'v' => s.push('\x0B'),
                            b'/' => s.push('/'),
                            b'\\' => s.push('\\'),
                            b'"' => s.push('"'),
                            other => {
                                s.push('\\');
                                s.push(other as char);
                            }
                        }
                        i += 2;
                    } else {
                        let ch = &expr[i..];
                        let c = ch.chars().next().unwrap();
                        s.push(c);
                        i += c.len_utf8();
                    }
                }
                tokens.push(ExprToken::DblStr(s));
                if i < bytes.len() {
                    i += 1;
                }
            }
            b'=' if i + 1 < bytes.len() && bytes[i + 1] == b'=' => {
                tokens.push(ExprToken::Eq);
                i += 2;
            }
            b'!' if i + 1 < bytes.len() && bytes[i + 1] == b'=' => {
                tokens.push(ExprToken::Ne);
                i += 2;
            }
            b'!' => {
                tokens.push(ExprToken::Not);
                i += 1;
            }
            b'<' if i + 1 < bytes.len() && bytes[i + 1] == b'=' => {
                tokens.push(ExprToken::Le);
                i += 2;
            }
            b'<' => {
                tokens.push(ExprToken::Lt);
                i += 1;
            }
            b'>' if i + 1 < bytes.len() && bytes[i + 1] == b'=' => {
                tokens.push(ExprToken::Ge);
                i += 2;
            }
            b'>' => {
                tokens.push(ExprToken::Gt);
                i += 1;
            }
            b'&' if i + 1 < bytes.len() && bytes[i + 1] == b'&' => {
                tokens.push(ExprToken::And);
                i += 2;
            }
            b'|' if i + 1 < bytes.len() && bytes[i + 1] == b'|' => {
                tokens.push(ExprToken::Or);
                i += 2;
            }
            b'+' => {
                tokens.push(ExprToken::Plus);
                i += 1;
            }
            b'-' => {
                tokens.push(ExprToken::Minus);
                i += 1;
            }
            b'*' => {
                tokens.push(ExprToken::Star);
                i += 1;
            }
            b'/' => {
                tokens.push(ExprToken::Slash);
                i += 1;
            }
            b'%' => {
                tokens.push(ExprToken::Percent);
                i += 1;
            }
            b'(' => {
                tokens.push(ExprToken::LParen);
                i += 1;
            }
            b')' => {
                tokens.push(ExprToken::RParen);
                i += 1;
            }
            b',' => {
                tokens.push(ExprToken::Comma);
                i += 1;
            }
            c if c.is_ascii_digit()
                || (c == b'.' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit()) =>
            {
                let start = i;
                while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
                    i += 1;
                }
                if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
                    i += 1;
                    if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
                        i += 1;
                    }
                    while i < bytes.len() && bytes[i].is_ascii_digit() {
                        i += 1;
                    }
                }
                let num_str = String::from_utf8_lossy(&bytes[start..i]);
                if let Ok(n) = num_str.parse::<f64>() {
                    tokens.push(ExprToken::Number(n));
                }
            }
            c if c.is_ascii_alphanumeric() || c == b'_' || c == b'.' => {
                let start = i;
                while i < bytes.len()
                    && (bytes[i].is_ascii_alphanumeric()
                        || bytes[i] == b'_'
                        || bytes[i] == b'.'
                        || bytes[i] == b'-')
                {
                    i += 1;
                }
                let word = String::from_utf8_lossy(&bytes[start..i]).to_string();
                tokens.push(ExprToken::Ident(word));
            }
            _ => i += 1,
        }
    }

    tokens
}

/// Return all identifier tokens from an expression string.
pub fn extract_identifiers(expr: &str) -> Vec<String> {
    tokenize_expression(expr)
        .into_iter()
        .filter_map(|t| match t {
            ExprToken::Ident(id) => Some(id),
            _ => None,
        })
        .collect()
}

/// Whether a string value is truthy (non-empty, not "false", not "0").
pub fn is_truthy(s: &str) -> bool {
    !s.is_empty() && s != "false" && s != "0"
}

fn try_as_number(s: &str) -> Option<f64> {
    s.parse::<f64>().ok()
}

/// Evaluate a `${{ }}` expression string, returning the result as a string.
pub fn eval_expression(expr: &str, ctx: &dyn ExprContext) -> String {
    let tokens = tokenize_expression(expr);
    let mut pos = 0;
    eval_or_expr(&tokens, &mut pos, ctx)
}

// or_expr = and_expr ('||' and_expr)*
fn eval_or_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    let mut left = eval_and_expr(tokens, pos, ctx);
    while *pos < tokens.len() && matches!(&tokens[*pos], ExprToken::Or) {
        *pos += 1;
        let right = eval_and_expr(tokens, pos, ctx);
        left = if is_truthy(&left) { left } else { right };
    }
    left
}

// and_expr = cmp_expr ('&&' cmp_expr)*
fn eval_and_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    let mut left = eval_cmp_expr(tokens, pos, ctx);
    while *pos < tokens.len() && matches!(&tokens[*pos], ExprToken::And) {
        *pos += 1;
        let right = eval_cmp_expr(tokens, pos, ctx);
        left = if is_truthy(&left) {
            right
        } else {
            String::new()
        };
    }
    left
}

// cmp_expr = add_expr (('==' | '!=' | '<' | '<=' | '>' | '>=') add_expr)?
fn eval_cmp_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    let left = eval_add_expr(tokens, pos, ctx);
    if *pos >= tokens.len() {
        return left;
    }
    match &tokens[*pos] {
        ExprToken::Eq => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(left == right)
        }
        ExprToken::Ne => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(left != right)
        }
        ExprToken::Lt => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(cmp_values(&left, &right) == Some(std::cmp::Ordering::Less))
        }
        ExprToken::Le => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(matches!(
                cmp_values(&left, &right),
                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
            ))
        }
        ExprToken::Gt => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(cmp_values(&left, &right) == Some(std::cmp::Ordering::Greater))
        }
        ExprToken::Ge => {
            *pos += 1;
            let right = eval_add_expr(tokens, pos, ctx);
            bool_str(matches!(
                cmp_values(&left, &right),
                Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
            ))
        }
        _ => left,
    }
}

fn bool_str(b: bool) -> String {
    if b { "true".to_string() } else { String::new() }
}

fn cmp_values(a: &str, b: &str) -> Option<std::cmp::Ordering> {
    if let (Some(na), Some(nb)) = (try_as_number(a), try_as_number(b)) {
        na.partial_cmp(&nb)
    } else {
        Some(a.cmp(b))
    }
}

// add_expr = mul_expr (('+' | '-') mul_expr)*
fn eval_add_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    let mut left = eval_mul_expr(tokens, pos, ctx);
    while *pos < tokens.len() {
        match &tokens[*pos] {
            ExprToken::Plus => {
                *pos += 1;
                let right = eval_mul_expr(tokens, pos, ctx);
                left = match (try_as_number(&left), try_as_number(&right)) {
                    (Some(a), Some(b)) => format_number(a + b),
                    _ => format!("{}{}", left, right),
                };
            }
            ExprToken::Minus => {
                *pos += 1;
                let right = eval_mul_expr(tokens, pos, ctx);
                left = match (try_as_number(&left), try_as_number(&right)) {
                    (Some(a), Some(b)) => format_number(a - b),
                    _ => left,
                };
            }
            _ => break,
        }
    }
    left
}

// mul_expr = unary_expr (('*' | '/' | '%') unary_expr)*
fn eval_mul_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    let mut left = eval_unary_expr(tokens, pos, ctx);
    while *pos < tokens.len() {
        match &tokens[*pos] {
            ExprToken::Star => {
                *pos += 1;
                let right = eval_unary_expr(tokens, pos, ctx);
                left = match (try_as_number(&left), try_as_number(&right)) {
                    (Some(a), Some(b)) => format_number(a * b),
                    _ => left,
                };
            }
            ExprToken::Slash => {
                *pos += 1;
                let right = eval_unary_expr(tokens, pos, ctx);
                left = match (try_as_number(&left), try_as_number(&right)) {
                    (Some(a), Some(b)) if b != 0.0 => format_number(a / b),
                    _ => left,
                };
            }
            ExprToken::Percent => {
                *pos += 1;
                let right = eval_unary_expr(tokens, pos, ctx);
                left = match (try_as_number(&left), try_as_number(&right)) {
                    (Some(a), Some(b)) if b != 0.0 => format_number(a % b),
                    _ => left,
                };
            }
            _ => break,
        }
    }
    left
}

// unary_expr = ('!' | '-' | '+') unary_expr | atom
fn eval_unary_expr(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    if *pos >= tokens.len() {
        return String::new();
    }
    match &tokens[*pos] {
        ExprToken::Not => {
            *pos += 1;
            let val = eval_unary_expr(tokens, pos, ctx);
            bool_str(!is_truthy(&val))
        }
        ExprToken::Minus => {
            *pos += 1;
            let val = eval_unary_expr(tokens, pos, ctx);
            match try_as_number(&val) {
                Some(n) => format_number(-n),
                None => val,
            }
        }
        ExprToken::Plus => {
            *pos += 1;
            eval_unary_expr(tokens, pos, ctx)
        }
        _ => eval_atom(tokens, pos, ctx),
    }
}

// atom = Str | DblStr | Number | Ident ['(' args ')'] | '(' or_expr ')'
fn eval_atom(tokens: &[ExprToken], pos: &mut usize, ctx: &dyn ExprContext) -> String {
    if *pos >= tokens.len() {
        return String::new();
    }
    match &tokens[*pos] {
        ExprToken::Str(s) => {
            let v = s.clone();
            *pos += 1;
            v
        }
        ExprToken::DblStr(s) => {
            let v = s.clone();
            *pos += 1;
            v
        }
        ExprToken::Number(n) => {
            let v = format_number(*n);
            *pos += 1;
            v
        }
        ExprToken::Ident(id) => {
            let id = id.clone();
            *pos += 1;
            if *pos < tokens.len() && matches!(&tokens[*pos], ExprToken::LParen) {
                *pos += 1; // skip '('
                let arg = eval_or_expr(tokens, pos, ctx);
                if *pos < tokens.len() && matches!(&tokens[*pos], ExprToken::RParen) {
                    *pos += 1; // skip ')'
                }
                eval_builtin(&id, &arg)
            } else {
                match id.as_str() {
                    "true" => "true".to_string(),
                    "false" | "null" => String::new(),
                    _ => ctx.resolve(&id).unwrap_or_default(),
                }
            }
        }
        ExprToken::LParen => {
            *pos += 1;
            let val = eval_or_expr(tokens, pos, ctx);
            if *pos < tokens.len() && matches!(&tokens[*pos], ExprToken::RParen) {
                *pos += 1;
            }
            val
        }
        _ => {
            *pos += 1;
            String::new()
        }
    }
}

fn eval_builtin(name: &str, arg: &str) -> String {
    match name {
        "str" => arg.to_string(),
        "num" => match try_as_number(arg) {
            Some(n) => format_number(n),
            None => "0".to_string(),
        },
        "bool" => {
            if is_truthy(arg) {
                "true".to_string()
            } else {
                "false".to_string()
            }
        }
        _ => String::new(),
    }
}

fn format_number(n: f64) -> String {
    if n == n.trunc() && n.abs() < 1e15 {
        format!("{}", n as i64)
    } else {
        format!("{}", n)
    }
}

/// Substitute `${{ }}` expressions in a string.
/// Supports `\${{` to escape and produce a literal `${{`.
pub fn expand_expressions(s: &str, ctx: &dyn ExprContext) -> String {
    if !s.contains("${{") {
        return s.to_string();
    }
    let mut result = s.to_string();
    let mut search_pos = 0;
    while search_pos < result.len() {
        if let Some(offset) = result[search_pos..].find("${{") {
            let start = search_pos + offset;
            if start > 0 && result.as_bytes()[start - 1] == b'\\' {
                result.replace_range(start - 1..start, "");
                search_pos = start + 2;
                continue;
            }
            if let Some(end_offset) = result[start..].find("}}") {
                let end = start + end_offset + 2;
                let expr = result[start + 3..start + end_offset].trim();
                let value = eval_expression(expr, ctx);
                result.replace_range(start..end, &value);
                search_pos = start + value.len();
            } else {
                break;
            }
        } else {
            break;
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    struct TestCtx(HashMap<String, String>);

    impl ExprContext for TestCtx {
        fn resolve(&self, id: &str) -> Option<String> {
            self.0.get(id).cloned()
        }
    }

    fn ctx(pairs: &[(&str, &str)]) -> TestCtx {
        TestCtx(
            pairs
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        )
    }

    fn empty_ctx() -> TestCtx {
        TestCtx(HashMap::new())
    }

    #[test]
    fn test_tokenize_basic() {
        let tokens = tokenize_expression("a == 'hello'");
        assert_eq!(tokens.len(), 3);
        assert!(matches!(&tokens[0], ExprToken::Ident(s) if s == "a"));
        assert!(matches!(&tokens[1], ExprToken::Eq));
        assert!(matches!(&tokens[2], ExprToken::Str(s) if s == "hello"));
    }

    #[test]
    fn test_tokenize_double_quoted() {
        let tokens = tokenize_expression(r#""hello\nworld""#);
        assert_eq!(tokens.len(), 1);
        assert!(matches!(&tokens[0], ExprToken::DblStr(s) if s == "hello\nworld"));
    }

    #[test]
    fn test_tokenize_number() {
        let tokens = tokenize_expression("42 + 3.14");
        assert_eq!(tokens.len(), 3);
        assert!(matches!(&tokens[0], ExprToken::Number(n) if *n == 42.0));
        assert!(matches!(&tokens[1], ExprToken::Plus));
        assert!(matches!(&tokens[2], ExprToken::Number(n) if (*n - 3.14).abs() < 0.001));
    }

    #[test]
    fn test_tokenize_operators() {
        let tokens = tokenize_expression("a != b && c || !d < e <= f > g >= h");
        let kinds: Vec<&str> = tokens
            .iter()
            .map(|t| match t {
                ExprToken::Ident(_) => "id",
                ExprToken::Ne => "!=",
                ExprToken::And => "&&",
                ExprToken::Or => "||",
                ExprToken::Not => "!",
                ExprToken::Lt => "<",
                ExprToken::Le => "<=",
                ExprToken::Gt => ">",
                ExprToken::Ge => ">=",
                _ => "?",
            })
            .collect();
        assert_eq!(
            kinds,
            vec![
                "id", "!=", "id", "&&", "id", "||", "!", "id", "<", "id", "<=", "id", ">", "id",
                ">=", "id"
            ]
        );
    }

    #[test]
    fn test_tokenize_arithmetic() {
        let tokens = tokenize_expression("a + b - c * d / e % f");
        let kinds: Vec<&str> = tokens
            .iter()
            .map(|t| match t {
                ExprToken::Ident(_) => "id",
                ExprToken::Plus => "+",
                ExprToken::Minus => "-",
                ExprToken::Star => "*",
                ExprToken::Slash => "/",
                ExprToken::Percent => "%",
                _ => "?",
            })
            .collect();
        assert_eq!(
            kinds,
            vec!["id", "+", "id", "-", "id", "*", "id", "/", "id", "%", "id"]
        );
    }

    #[test]
    fn test_tokenize_parens() {
        let tokens = tokenize_expression("(a + b)");
        assert_eq!(tokens.len(), 5);
        assert!(matches!(&tokens[0], ExprToken::LParen));
        assert!(matches!(&tokens[4], ExprToken::RParen));
    }

    #[test]
    fn test_eval_ident() {
        let c = ctx(&[("inputs.name", "world")]);
        assert_eq!(eval_expression("inputs.name", &c), "world");
    }

    #[test]
    fn test_eval_ident_missing() {
        assert_eq!(eval_expression("inputs.missing", &empty_ctx()), "");
    }

    #[test]
    fn test_eval_or_default() {
        let c = empty_ctx();
        assert_eq!(eval_expression("inputs.name || 'fallback'", &c), "fallback");

        let c = ctx(&[("inputs.name", "world")]);
        assert_eq!(eval_expression("inputs.name || 'fallback'", &c), "world");
    }

    #[test]
    fn test_eval_and_chain() {
        let c = ctx(&[("a", "yes"), ("b", "ok")]);
        assert_eq!(eval_expression("a && b", &c), "ok");

        let c = ctx(&[("a", ""), ("b", "ok")]);
        assert_eq!(eval_expression("a && b", &c), "");
    }

    #[test]
    fn test_eval_eq_ne() {
        let c = ctx(&[("x", "hello")]);
        assert_eq!(eval_expression("x == 'hello'", &c), "true");
        assert_eq!(eval_expression("x == 'world'", &c), "");
        assert_eq!(eval_expression("x != 'world'", &c), "true");
        assert_eq!(eval_expression("x != 'hello'", &c), "");
    }

    #[test]
    fn test_eval_comparisons() {
        let c = ctx(&[("x", "10"), ("y", "20")]);
        assert_eq!(eval_expression("x < y", &c), "true");
        assert_eq!(eval_expression("x > y", &c), "");
        assert_eq!(eval_expression("x <= y", &c), "true");
        assert_eq!(eval_expression("x >= y", &c), "");
        assert_eq!(eval_expression("x <= x", &c), "true");
        assert_eq!(eval_expression("x >= x", &c), "true");
    }

    #[test]
    fn test_eval_string_concat() {
        let c = ctx(&[("a", "hello"), ("b", " world")]);
        assert_eq!(eval_expression("a + b", &c), "hello world");
    }

    #[test]
    fn test_eval_numeric_arithmetic() {
        let c = empty_ctx();
        assert_eq!(eval_expression("10 + 3", &c), "13");
        assert_eq!(eval_expression("10 - 3", &c), "7");
        assert_eq!(eval_expression("10 * 3", &c), "30");
        assert_eq!(eval_expression("10 / 4", &c), "2.5");
        assert_eq!(eval_expression("10 % 3", &c), "1");
    }

    #[test]
    fn test_eval_precedence() {
        let c = empty_ctx();
        // 2 + 3 * 4 = 14 (mul before add)
        assert_eq!(eval_expression("2 + 3 * 4", &c), "14");
        // (2 + 3) * 4 = 20 (parens override)
        assert_eq!(eval_expression("(2 + 3) * 4", &c), "20");
    }

    #[test]
    fn test_eval_not() {
        let c = ctx(&[("x", "hello")]);
        assert_eq!(eval_expression("!x", &c), "");
        assert_eq!(eval_expression("!missing", &c), "true");
    }

    #[test]
    fn test_eval_unary_minus() {
        let c = empty_ctx();
        assert_eq!(eval_expression("-5", &c), "-5");
        assert_eq!(eval_expression("-5 + 3", &c), "-2");
    }

    #[test]
    fn test_eval_builtin_str() {
        let c = empty_ctx();
        assert_eq!(eval_expression("str(42)", &c), "42");
    }

    #[test]
    fn test_eval_builtin_num() {
        let c = empty_ctx();
        assert_eq!(eval_expression("num('42')", &c), "42");
        assert_eq!(eval_expression("num('abc')", &c), "0");
    }

    #[test]
    fn test_eval_builtin_bool() {
        let c = empty_ctx();
        assert_eq!(eval_expression("bool('hello')", &c), "true");
        assert_eq!(eval_expression("bool('')", &c), "false");
        assert_eq!(eval_expression("bool('0')", &c), "false");
        assert_eq!(eval_expression("bool('false')", &c), "false");
    }

    #[test]
    fn test_eval_double_quoted_string() {
        let c = empty_ctx();
        assert_eq!(eval_expression(r#""hello""#, &c), "hello");
        assert_eq!(eval_expression(r#""hello" + " world""#, &c), "hello world");
    }

    #[test]
    fn test_expand_expressions() {
        let c = ctx(&[("inputs.name", "world")]);
        assert_eq!(
            expand_expressions("Hello ${{ inputs.name }}!", &c),
            "Hello world!"
        );
    }

    #[test]
    fn test_expand_expressions_multiple() {
        let c = ctx(&[("a", "X"), ("b", "Y")]);
        assert_eq!(expand_expressions("${{ a }}-${{ b }}", &c), "X-Y");
    }

    #[test]
    fn test_expand_expressions_no_match() {
        let c = empty_ctx();
        assert_eq!(
            expand_expressions("no expressions here", &c),
            "no expressions here"
        );
    }

    #[test]
    fn test_expand_expressions_with_default() {
        let c = empty_ctx();
        assert_eq!(
            expand_expressions("Hello ${{ inputs.name || 'default' }}!", &c),
            "Hello default!"
        );
    }

    #[test]
    fn test_is_truthy() {
        assert!(is_truthy("hello"));
        assert!(is_truthy("true"));
        assert!(is_truthy("1"));
        assert!(!is_truthy(""));
        assert!(!is_truthy("false"));
        assert!(!is_truthy("0"));
    }

    #[test]
    fn test_eval_nested_or_and() {
        let c = ctx(&[("a", ""), ("b", "yes"), ("c", "ok")]);
        // (a || b) && c → "yes" && "ok" → "ok"
        assert_eq!(eval_expression("(a || b) && c", &c), "ok");
    }

    #[test]
    fn test_eval_division_by_zero() {
        let c = empty_ctx();
        // Division by zero returns left operand unchanged
        assert_eq!(eval_expression("10 / 0", &c), "10");
        assert_eq!(eval_expression("10 % 0", &c), "10");
    }

    #[test]
    fn test_eval_string_comparison() {
        let c = empty_ctx();
        assert_eq!(eval_expression("'abc' < 'def'", &c), "true");
        assert_eq!(eval_expression("'def' < 'abc'", &c), "");
    }

    #[test]
    fn test_eval_true_false_null() {
        let c = empty_ctx();
        assert_eq!(eval_expression("true", &c), "true");
        assert_eq!(eval_expression("false", &c), "");
        assert_eq!(eval_expression("null", &c), "");
    }

    #[test]
    fn test_eval_not_keywords() {
        let c = empty_ctx();
        assert_eq!(eval_expression("!true", &c), "");
        assert_eq!(eval_expression("!false", &c), "true");
        assert_eq!(eval_expression("!null", &c), "true");
    }

    #[test]
    fn test_eval_keyword_equality() {
        let c = empty_ctx();
        assert_eq!(eval_expression("true == true", &c), "true");
        assert_eq!(eval_expression("false == false", &c), "true");
        assert_eq!(eval_expression("null == null", &c), "true");
        assert_eq!(eval_expression("true != false", &c), "true");
        assert_eq!(eval_expression("true == false", &c), "");
    }

    #[test]
    fn test_eval_or_with_keywords() {
        let c = empty_ctx();
        assert_eq!(eval_expression(r#"null || "default""#, &c), "default");
        assert_eq!(eval_expression(r#"false || "fb""#, &c), "fb");
        assert_eq!(eval_expression(r#"true || "other""#, &c), "true");
    }

    #[test]
    fn test_eval_and_with_keywords() {
        let c = empty_ctx();
        assert_eq!(eval_expression(r#"true && "yes""#, &c), "yes");
        assert_eq!(eval_expression(r#"false && "yes""#, &c), "");
    }

    #[test]
    fn test_escape_expression() {
        let c = empty_ctx();
        assert_eq!(
            expand_expressions(r"Hello \${{ nope }}", &c),
            "Hello ${{ nope }}"
        );
        assert_eq!(
            expand_expressions(r"a \${{ x }} b ${{ 'y' }} c", &ctx(&[])),
            "a ${{ x }} b y c"
        );
    }

    #[test]
    fn test_double_quoted_extra_escapes() {
        let c = empty_ctx();
        assert_eq!(eval_expression(r#""\r""#, &c), "\r");
        assert_eq!(eval_expression(r#""\a""#, &c), "\x07");
        assert_eq!(eval_expression(r#""\b""#, &c), "\x08");
        assert_eq!(eval_expression(r#""\f""#, &c), "\x0C");
        assert_eq!(eval_expression(r#""\v""#, &c), "\x0B");
        assert_eq!(eval_expression(r#""\/""#, &c), "/");
    }

    #[test]
    fn test_single_quoted_escapes() {
        let c = empty_ctx();
        assert_eq!(eval_expression(r"'it\'s'", &c), "it's");
        assert_eq!(eval_expression(r"'back\\slash'", &c), r"back\slash");
    }

    #[test]
    fn test_non_ascii_string_literals() {
        let c = empty_ctx();
        assert_eq!(eval_expression("'café'", &c), "café");
        assert_eq!(eval_expression(r#""héllo""#, &c), "héllo");
        assert_eq!(eval_expression("'日本語'", &c), "日本語");
    }

    #[test]
    fn test_tokenize_scientific() {
        let tokens = tokenize_expression("1.5e3");
        assert_eq!(tokens.len(), 1);
        assert!(matches!(&tokens[0], ExprToken::Number(n) if (*n - 1500.0).abs() < 0.001));

        let tokens = tokenize_expression("2E-4");
        assert_eq!(tokens.len(), 1);
        assert!(matches!(&tokens[0], ExprToken::Number(n) if (*n - 0.0002).abs() < 1e-10));

        let tokens = tokenize_expression("3e+2");
        assert_eq!(tokens.len(), 1);
        assert!(matches!(&tokens[0], ExprToken::Number(n) if (*n - 300.0).abs() < 0.001));
    }

    #[test]
    fn test_eval_scientific() {
        let c = empty_ctx();
        assert_eq!(eval_expression("1.5e3 + 500", &c), "2000");
        assert_eq!(eval_expression("2E-4 * 10000", &c), "2");
    }

    // --- Integration tests from Moa.md examples ---

    #[test]
    fn test_moa_deploy_strategy_selection() {
        // Moa.md line 454: image selection based on environment
        let prod = ctx(&[("job.inputs.environment", "production")]);
        assert_eq!(
            eval_expression(
                r#"job.inputs.environment == "production" && "deploy-tools:stable" || "deploy-tools:latest""#,
                &prod
            ),
            "deploy-tools:stable"
        );
        let staging = ctx(&[("job.inputs.environment", "staging")]);
        assert_eq!(
            eval_expression(
                r#"job.inputs.environment == "production" && "deploy-tools:stable" || "deploy-tools:latest""#,
                &staging
            ),
            "deploy-tools:latest"
        );
    }

    #[test]
    fn test_moa_conditional_flags() {
        // Moa.md line 475: pytest flags from boolean inputs
        let verbose = ctx(&[("job.inputs.verbose", "true")]);
        assert_eq!(
            eval_expression(r#"job.inputs.verbose && "-v" || """#, &verbose),
            "-v"
        );
        let quiet = ctx(&[("job.inputs.verbose", "false")]);
        assert_eq!(
            eval_expression(r#"job.inputs.verbose && "-v" || """#, &quiet),
            ""
        );

        let cov = ctx(&[("job.inputs.coverage", "true")]);
        assert_eq!(
            eval_expression(r#"job.inputs.coverage && "--cov=src" || """#, &cov),
            "--cov=src"
        );
        let no_cov = ctx(&[("job.inputs.coverage", "false")]);
        assert_eq!(
            eval_expression(r#"job.inputs.coverage && "--cov=src" || """#, &no_cov),
            ""
        );
    }

    #[test]
    fn test_moa_image_reference_building() {
        // Moa.md line 487: concatenating registry + path + tag
        let c = ctx(&[
            ("vars.CI_REGISTRY", "registry.example.com"),
            ("vars.CI_PROJECT_PATH", "group/project"),
            ("vars.CI_PIPELINE_IID", "42"),
        ]);
        assert_eq!(
            eval_expression(
                r#"vars.CI_REGISTRY + "/" + vars.CI_PROJECT_PATH + ":" + vars.CI_PIPELINE_IID"#,
                &c
            ),
            "registry.example.com/group/project:42"
        );
    }

    #[test]
    fn test_moa_continue_gate() {
        // Moa.md line 499: quality gate based on scan outputs
        let pass = ctx(&[
            ("steps.scan.outputs.critical", "0"),
            ("steps.scan.outputs.high", "3"),
        ]);
        assert_eq!(
            eval_expression(
                "steps.scan.outputs.critical == 0 && steps.scan.outputs.high < 5",
                &pass
            ),
            "true"
        );
        let fail = ctx(&[
            ("steps.scan.outputs.critical", "1"),
            ("steps.scan.outputs.high", "3"),
        ]);
        assert_eq!(
            eval_expression(
                "steps.scan.outputs.critical == 0 && steps.scan.outputs.high < 5",
                &fail
            ),
            ""
        );
    }

    #[test]
    fn test_moa_version_management() {
        // Moa.md line 513: bump major version
        let c = ctx(&[("steps.current.outputs.major", "3")]);
        assert_eq!(
            eval_expression(r#"str(steps.current.outputs.major + 1) + ".0.0""#, &c),
            "4.0.0"
        );
    }

    #[test]
    fn test_moa_env_specific_config() {
        // Moa.md line 523: environment-specific registry and replicas
        let main = ctx(&[("vars.CI_COMMIT_REF_NAME", "main")]);
        assert_eq!(
            eval_expression(
                r#"(vars.CI_COMMIT_REF_NAME == "main" && "prod.registry.com") || "staging.registry.com""#,
                &main
            ),
            "prod.registry.com"
        );
        let dev = ctx(&[("vars.CI_COMMIT_REF_NAME", "dev")]);
        assert_eq!(
            eval_expression(
                r#"(vars.CI_COMMIT_REF_NAME == "main" && "prod.registry.com") || "staging.registry.com""#,
                &dev
            ),
            "staging.registry.com"
        );

        assert_eq!(
            eval_expression(r#"(vars.CI_COMMIT_REF_NAME == "main" && 5) || 2"#, &main),
            "5"
        );
        assert_eq!(
            eval_expression(r#"(vars.CI_COMMIT_REF_NAME == "main" && 5) || 2"#, &dev),
            "2"
        );
    }

    #[test]
    fn test_moa_keyword_integration() {
        let c = empty_ctx();
        assert_eq!(eval_expression("bool(0)", &c), "false");
        assert_eq!(eval_expression("bool(1)", &c), "true");
        assert_eq!(eval_expression("!false", &c), "true");
        assert_eq!(eval_expression(r#"true && "yes""#, &c), "yes");
        assert_eq!(eval_expression(r#"false || "fallback""#, &c), "fallback");
        assert_eq!(eval_expression(r#"null || "default""#, &c), "default");
    }
}
