//! Integration tests for kaish REPL.
//!
//! These tests run scripts through the REPL and verify behavior.

use kaish_repl::{ProcessResult, Repl};

/// Helper to run multiple lines through a REPL and collect outputs.
///
/// For multi-line constructs (if/fi, for/done, case/esac, while/done),
/// lines are joined before parsing so that the parser can handle them
/// as complete statements.
fn run_script(script: &str) -> Vec<String> {
    let mut repl = Repl::new().expect("Failed to create REPL");
    let mut outputs = Vec::new();

    // Collect lines, joining multi-line constructs
    let mut current_block = String::new();
    let mut block_depth: usize = 0;

    for line in script.lines() {
        // Skip comments and empty lines at the top level
        let trimmed = line.trim();
        if block_depth == 0 && (trimmed.is_empty() || trimmed.starts_with('#')) {
            continue;
        }

        // Track block depth for multi-line constructs
        // Count opening keywords (if, for, while, case)
        for word in trimmed.split_whitespace() {
            match word {
                "if" | "for" | "while" | "case" => block_depth += 1,
                "fi" | "done" | "esac" => block_depth = block_depth.saturating_sub(1),
                _ => {}
            }
            // Handle semi-terminated keywords like "fi;" or "done;"
            let word_clean = word.trim_end_matches(';');
            match word_clean {
                "fi" | "done" | "esac" if word != word_clean => {
                    // Already counted above, but with semicolon
                }
                _ => {}
            }
        }

        // Append line to current block
        if !current_block.is_empty() {
            current_block.push('\n');
        }
        current_block.push_str(trimmed);

        // If we're at top level (depth 0), process the block
        if block_depth == 0 && !current_block.is_empty() {
            match repl.process_line(&current_block) {
                ProcessResult::Output(output) => outputs.push(output),
                ProcessResult::Empty => {}
                ProcessResult::Exit => break,
            }
            current_block.clear();
        }
    }

    // Process any remaining content
    if !current_block.is_empty() {
        match repl.process_line(&current_block) {
            ProcessResult::Output(output) => outputs.push(output),
            ProcessResult::Empty => {}
            ProcessResult::Exit => {}
        }
    }

    outputs
}

/// Helper to check if output contains expected strings.
fn outputs_contain(outputs: &[String], expected: &[&str]) -> bool {
    let joined = outputs.join("\n");
    expected.iter().all(|e| joined.contains(e))
}

// ============================================================================
// Scope Tests
// ============================================================================

#[test]
fn scope_basic_variable() {
    let outputs = run_script(r#"
        X=42
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["42"]));
}

#[test]
fn scope_variable_shadowing_in_loop() {
    let outputs = run_script(r#"
        X="outer"
        for I in "inner"; do X=${I}; echo ${X}; done
        echo ${X}
    "#);
    // Note: Current behavior - X in loop is in inner frame, so outer X unchanged
    // This tests ACTUAL behavior, not necessarily DESIRED behavior
    let joined = outputs.join("\n");
    assert!(joined.contains("inner"), "Should print inner inside loop. Output was: {}", joined);
}

#[test]
fn scope_json_as_string() {
    // Objects are now stored as JSON strings, processed with jq
    let outputs = run_script(r#"
        DATA='{"user": {"name": "Alice"}}'
        echo ${DATA}
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("Alice"), "JSON string should contain Alice. Output was: {}", joined);
}

#[test]
fn scope_last_result_propagation() {
    let outputs = run_script(r#"
        echo "first"
        echo "code was ${?.code}"
    "#);
    assert!(outputs_contain(&outputs, &["first", "code was 0"]));
}

#[test]
fn scope_last_result_fields() {
    let outputs = run_script(r#"
        echo "test output"
        echo "ok=${?.ok}"
    "#);
    assert!(outputs_contain(&outputs, &["ok=true"]));
}

// ============================================================================
// Interpolation Tests
// ============================================================================

#[test]
fn interpolation_basic() {
    let outputs = run_script(r#"
        NAME="World"
        echo "Hello ${NAME}"
    "#);
    assert!(outputs_contain(&outputs, &["Hello World"]));
}

#[test]
fn interpolation_empty_string() {
    let outputs = run_script(r#"
        EMPTY=""
        echo "before${EMPTY}after"
    "#);
    assert!(outputs_contain(&outputs, &["beforeafter"]));
}

#[test]
fn interpolation_multiple_vars() {
    let outputs = run_script(r#"
        A="one"
        B="two"
        echo "${A} ${B}"
    "#);
    assert!(outputs_contain(&outputs, &["one two"]));
}

#[test]
fn interpolation_adjacent_no_space() {
    let outputs = run_script(r#"
        A="one"
        B="two"
        echo "${A}${B}"
    "#);
    assert!(outputs_contain(&outputs, &["onetwo"]));
}

#[test]
fn interpolation_json_string() {
    // Objects are now JSON strings
    let outputs = run_script(r#"
        OBJ='{"inner": {"value": "nested"}}'
        echo "got: ${OBJ}"
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("nested"), "JSON string should contain nested. Output was: {}", joined);
}

#[test]
fn interpolation_word_split() {
    // Explicit split required — kaish has no implicit word splitting
    let outputs = run_script(r#"
        ITEMS="zero one two"
        for I in $(split ${ITEMS}); do echo ${I}; done
    "#);
    assert!(outputs_contain(&outputs, &["zero", "one", "two"]));
}

#[test]
fn interpolation_number() {
    let outputs = run_script(r#"
        NUM=42
        echo "num=${NUM}"
    "#);
    assert!(outputs_contain(&outputs, &["num=42"]));
}

#[test]
fn interpolation_boolean() {
    let outputs = run_script(r#"
        FLAG=true
        echo "flag=${FLAG}"
    "#);
    assert!(outputs_contain(&outputs, &["flag=true"]));
}

#[test]
fn interpolation_null() {
    let outputs = run_script(r#"
        NOTHING=null
        echo "val=${NOTHING}"
    "#);
    assert!(outputs_contain(&outputs, &["val=null"]));
}

// ============================================================================
// Expression Tests
// ============================================================================

#[test]
fn expr_equality() {
    // Shell-compatible: use [[ ]] for comparisons
    let outputs = run_script(r#"
        X=5
        if [[ ${X} == 5 ]]; then echo "equal"; fi
    "#);
    assert!(outputs_contain(&outputs, &["equal"]));
}

#[test]
fn expr_inequality() {
    let outputs = run_script(r#"
        X=5
        if [[ ${X} != 3 ]]; then echo "not equal"; fi
    "#);
    assert!(outputs_contain(&outputs, &["not equal"]));
}

#[test]
fn expr_less_than() {
    let outputs = run_script(r#"
        if [[ 3 -lt 5 ]]; then echo "less"; fi
    "#);
    assert!(outputs_contain(&outputs, &["less"]));
}

#[test]
fn expr_greater_than() {
    let outputs = run_script(r#"
        if [[ 5 -gt 3 ]]; then echo "greater"; fi
    "#);
    assert!(outputs_contain(&outputs, &["greater"]));
}

#[test]
fn expr_and_short_circuit() {
    let outputs = run_script(r#"
        if true && true; then echo "both true"; fi
        if true && false; then echo "wrong"; else echo "short circuit"; fi
    "#);
    assert!(outputs_contain(&outputs, &["both true", "short circuit"]));
}

#[test]
fn expr_or_short_circuit() {
    let outputs = run_script(r#"
        if false || true; then echo "found true"; fi
        if true || false; then echo "first true"; fi
    "#);
    assert!(outputs_contain(&outputs, &["found true", "first true"]));
}

#[test]
fn expr_precedence_and_or() {
    // && binds tighter than ||
    // true || false && false  =  true || (false && false)  =  true || false  =  true
    // Shell-compatible: command chaining with && and ||
    let outputs = run_script(r#"
        if true || false && false; then echo "precedence ok"; fi
    "#);
    assert!(outputs_contain(&outputs, &["precedence ok"]));
}

#[test]
fn expr_int_float_comparison() {
    // Shell-compatible: use [[ ]] for comparisons
    let outputs = run_script(r#"
        I=5
        F=5.0
        if [[ ${I} == ${F} ]]; then echo "int equals float"; fi
    "#);
    assert!(outputs_contain(&outputs, &["int equals float"]));
}

#[test]
fn expr_string_comparison() {
    // Shell-compatible: use [[ ]] with == for string comparison
    // Note: lexicographic < comparison may have parsing issues in REPL context
    let outputs = run_script(r#"
        if [[ "apple" != "banana" ]]; then echo "apple different"; fi
    "#);
    let joined = outputs.join("\n");
    assert!(outputs_contain(&outputs, &["apple different"]), "Output was: {}", joined);
}

#[test]
fn expr_truthiness_zero() {
    // Shell-compatible: test zero with explicit comparison
    let outputs = run_script(r#"
        if [[ 0 == 0 ]]; then echo "zero is zero"; fi
        if [[ 0 != 1 ]]; then echo "zero not one"; fi
    "#);
    assert!(outputs_contain(&outputs, &["zero is zero", "zero not one"]));
}

#[test]
fn expr_truthiness_empty_string() {
    // Shell-compatible: use [[ -z ]] to test for empty string
    let outputs = run_script(r#"
        if [[ -z "" ]]; then echo "empty falsy"; fi
    "#);
    assert!(outputs_contain(&outputs, &["empty falsy"]));
}

#[test]
fn expr_truthiness_null() {
    // Shell-compatible: test zero value with explicit comparison
    let outputs = run_script(r#"
        ZERO=0
        if [[ ${ZERO} == 0 ]]; then echo "zero falsy"; fi
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("zero falsy"), "Output was: {}", joined);
}

#[test]
fn expr_truthiness_empty_string_var() {
    // Shell-compatible: use [[ -z ]] to test empty string variable
    let outputs = run_script(r#"
        EMPTY=""
        if [[ -z ${EMPTY} ]]; then echo "empty var falsy"; fi
    "#);
    assert!(outputs_contain(&outputs, &["empty var falsy"]));
}

#[test]
fn expr_truthiness_non_empty_string() {
    // Shell-compatible: use [[ -n ]] to test non-empty string
    let outputs = run_script(r#"
        STR="hello"
        if [[ -n ${STR} ]]; then echo "string truthy"; fi
    "#);
    assert!(outputs_contain(&outputs, &["string truthy"]));
}

// ============================================================================
// Control Flow Tests
// ============================================================================

#[test]
fn control_if_then() {
    let outputs = run_script(r#"
        if true; then echo "yes"; fi
    "#);
    assert!(outputs_contain(&outputs, &["yes"]));
}

#[test]
fn control_if_else() {
    let outputs = run_script(r#"
        if false; then echo "wrong"; else echo "else branch"; fi
    "#);
    assert!(outputs_contain(&outputs, &["else branch"]));
}

#[test]
fn control_nested_if() {
    // Shell-compatible: use [[ ]] for comparisons
    let outputs = run_script(r#"
        X=5
        if [[ ${X} -gt 0 ]]; then
            if [[ ${X} -lt 10 ]]; then
                echo "in range"
            fi
        fi
    "#);
    assert!(outputs_contain(&outputs, &["in range"]));
}

#[test]
fn control_for_loop() {
    // kaish requires explicit split for word splitting (no implicit splitting)
    let outputs = run_script(r#"
        for I in $(split "1 2 3"); do echo ${I}; done
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("1"), "Output was: {}", joined);
    assert!(joined.contains("2"), "Output was: {}", joined);
    assert!(joined.contains("3"), "Output was: {}", joined);
}

#[test]
fn control_nested_loops() {
    // kaish requires explicit split for word splitting
    let outputs = run_script(r#"
        for I in $(split "1 2"); do for J in $(split "a b"); do echo "${I}-${J}"; done; done
    "#);
    assert!(outputs_contain(&outputs, &["1-a", "1-b", "2-a", "2-b"]));
}

#[test]
fn control_empty_loop() {
    // Empty split produces no iterations
    let outputs = run_script(r#"
        EMPTY=""
        for I in $(split ${EMPTY}); do echo "never"; done
        echo "after"
    "#);
    assert!(outputs_contain(&outputs, &["after"]));
    assert!(!outputs_contain(&outputs, &["never"]));
}

#[test]
fn control_loop_with_conditional() {
    // kaish requires explicit split; values are strings, compare with string "2"
    let outputs = run_script(r#"
        for I in $(split "1 2 3"); do if [[ ${I} == "2" ]]; then echo "found two"; fi; done
    "#);
    assert!(outputs_contain(&outputs, &["found two"]));
}

// ============================================================================
// Command Substitution Tests
// ============================================================================

#[test]
fn cmd_subst_basic() {
    // Command substitution captures stdout as a string
    let outputs = run_script(r#"
        R=$(echo "hello")
        echo ${R}
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("hello"), "Should have captured output. Output was: {}", joined);
}

#[test]
fn cmd_subst_last_result() {
    // $? returns last command exit code
    let outputs = run_script(r#"
        echo "hello"
        echo "code was ${?}"
    "#);
    assert!(outputs_contain(&outputs, &["code was 0"]));
}

// ============================================================================
// Error Handling Tests
// ============================================================================

#[test]
fn error_undefined_variable() {
    let outputs = run_script(r#"
        echo ${UNDEFINED}
    "#);
    let joined = outputs.join("\n");
    // Should produce an error, not crash
    assert!(joined.contains("ERROR") || joined.contains("undefined"));
}

#[test]
fn error_invalid_path() {
    let outputs = run_script(r#"
        X=42
        echo ${X.field}
    "#);
    let joined = outputs.join("\n");
    // Should error on field access of int
    assert!(joined.contains("ERROR") || joined.contains("undefined"));
}

#[test]
fn error_invalid_field_access() {
    let outputs = run_script(r#"
        NUM=42
        echo ${NUM.field}
    "#);
    let joined = outputs.join("\n");
    // Should error on field access of non-object
    assert!(joined.contains("ERROR") || joined.contains("undefined"));
}

// ============================================================================
// Unicode Tests
// ============================================================================

#[test]
fn unicode_basic() {
    let outputs = run_script(r#"
        echo "Hello, 世界!"
    "#);
    assert!(outputs_contain(&outputs, &["Hello, 世界!"]));
}

#[test]
fn unicode_emoji() {
    let outputs = run_script(r#"
        echo "🎉🚀✨"
    "#);
    assert!(outputs_contain(&outputs, &["🎉🚀✨"]));
}

#[test]
fn unicode_in_variable() {
    let outputs = run_script(r#"
        GREETING="こんにちは"
        echo ${GREETING}
    "#);
    assert!(outputs_contain(&outputs, &["こんにちは"]));
}

// ============================================================================
// Stress Tests
// ============================================================================

#[test]
fn stress_many_variables() {
    let outputs = run_script(r#"
        V1=1
        V2=2
        V3=3
        V4=4
        V5=5
        echo "${V1}${V2}${V3}${V4}${V5}"
    "#);
    assert!(outputs_contain(&outputs, &["12345"]));
}

#[test]
fn stress_json_string() {
    // Complex JSON stored as string
    let outputs = run_script(r#"
        D='{"a": {"b": {"c": {"d": "deep"}}}}'
        echo ${D}
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("deep"), "JSON string should contain deep. Output was: {}", joined);
}

#[test]
fn stress_many_items() {
    // Explicit split — kaish has no implicit word splitting
    let outputs = run_script(r#"
        ITEMS="1 2 3 4 5 6 7 8 9 10"
        for I in $(split ${ITEMS}); do echo ${I}; done
    "#);
    assert!(outputs_contain(&outputs, &["1", "5", "10"]));
}

#[test]
fn stress_complex_condition() {
    let outputs = run_script(r#"
        if true && true && true && true && true; then
            if false || false || false || true; then
                echo "complex passed"
            fi
        fi
    "#);
    assert!(outputs_contain(&outputs, &["complex passed"]));
}

// ============================================================================
// Introspection Builtin Tests
// ============================================================================

#[test]
fn introspect_vars_shows_set_variables() {
    let outputs = run_script(r#"
        X=42
        NAME="Alice"
        vars
    "#);
    let joined = outputs.join("\n");
    // Table format: NAME\tVALUE\tTYPE (canonical TSV from OutputData)
    assert!(joined.contains("X"), "vars should show X. Output was: {}", joined);
    assert!(joined.contains("42"), "vars should show 42. Output was: {}", joined);
    assert!(joined.contains("NAME"), "vars should show NAME. Output was: {}", joined);
}

#[test]
fn introspect_vars_json_format() {
    let outputs = run_script(r#"
        COUNT=100
        vars --json
    "#);
    let joined = outputs.join("\n");
    // Global --json produces table-keyed JSON: [{"NAME": ..., "VALUE": ..., "TYPE": ...}]
    assert!(joined.contains("\"NAME\""), "vars --json should have NAME field. Output was: {}", joined);
    assert!(joined.contains("\"VALUE\""), "vars --json should have VALUE field. Output was: {}", joined);
    assert!(joined.contains("COUNT"), "vars --json should include COUNT. Output was: {}", joined);
}

#[test]
fn introspect_tools_lists_builtins() {
    let outputs = run_script(r#"
        tools
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("echo"), "tools should list echo. Output was: {}", joined);
    assert!(joined.contains("ls"), "tools should list ls. Output was: {}", joined);
    assert!(joined.contains("cat"), "tools should list cat. Output was: {}", joined);
    assert!(joined.contains("vars"), "tools should list vars. Output was: {}", joined);
}

#[test]
fn introspect_tools_json_format() {
    let outputs = run_script(r#"
        tools --json
    "#);
    let joined = outputs.join("\n");
    // Global --json produces table-keyed JSON: [{"NAME": ..., "DESCRIPTION": ..., "PARAMS": ...}]
    assert!(joined.contains("\"NAME\""), "tools --json should have NAME field. Output was: {}", joined);
    assert!(joined.contains("\"DESCRIPTION\""), "tools --json should have DESCRIPTION field. Output was: {}", joined);
    // Should contain JSON array structure
    assert!(joined.contains('[') && joined.contains(']'), "tools --json should return array. Output was: {}", joined);
}

#[test]
fn introspect_tools_detail() {
    let outputs = run_script(r#"
        tools echo
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("echo"), "tools echo should show echo info. Output was: {}", joined);
}

#[test]
fn introspect_mounts_shows_vfs() {
    let outputs = run_script(r#"
        mounts
    "#);
    let joined = outputs.join("\n");
    // Should show at least the root mount
    assert!(joined.contains("/"), "mounts should show root. Output was: {}", joined);
    // Should indicate read-write or read-only
    assert!(joined.contains("rw") || joined.contains("ro"), "mounts should show mode. Output was: {}", joined);
}

#[test]
fn introspect_mounts_json_format() {
    let outputs = run_script(r#"
        mounts --json
    "#);
    let joined = outputs.join("\n");
    // Global --json produces table-keyed JSON: [{"PATH": ..., "MODE": ...}]
    assert!(joined.contains("\"PATH\""), "mounts --json should have PATH. Output was: {}", joined);
    assert!(joined.contains("\"MODE\""), "mounts --json should have MODE. Output was: {}", joined);
}

// ============================================================================
// Case Statement Integration Tests
// ============================================================================

#[test]
fn case_simple_match() {
    let outputs = run_script(r#"
        EXT="rs"
        case ${EXT} in
            rs) echo "Rust" ;;
            py) echo "Python" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["Rust"]));
}

#[test]
fn case_wildcard_match() {
    let outputs = run_script(r#"
        FILE="main.rs"
        case ${FILE} in
            *.py) echo "Python" ;;
            *.rs) echo "Rust" ;;
            *) echo "Unknown" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["Rust"]));
}

#[test]
fn case_default_fallthrough() {
    let outputs = run_script(r#"
        FILE="data.json"
        case ${FILE} in
            *.py) echo "Python" ;;
            *.rs) echo "Rust" ;;
            *) echo "Other" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["Other"]));
}

#[test]
fn case_no_match() {
    let outputs = run_script(r#"
        X="nomatch"
        case ${X} in
            foo) echo "foo" ;;
            bar) echo "bar" ;;
        esac
        echo "done"
    "#);
    // Should not match anything, then continue to echo done
    assert!(outputs_contain(&outputs, &["done"]));
    let joined = outputs.join("\n");
    assert!(!joined.contains("foo") && !joined.contains("bar"));
}

#[test]
fn case_multiple_patterns() {
    // Test multiple patterns with | separator
    let outputs = run_script(r#"
        EXT="ts"
        case ${EXT} in
            js|ts|jsx|tsx) echo "JavaScript family" ;;
            py|pyc|pyw) echo "Python family" ;;
            *) echo "other" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["JavaScript family"]));
}

#[test]
fn case_char_class() {
    let outputs = run_script(r#"
        CH="B"
        case ${CH} in
            [a-z]) echo "lower" ;;
            [A-Z]) echo "upper" ;;
            [0-9]) echo "digit" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["upper"]));
}

#[test]
fn case_question_mark() {
    let outputs = run_script(r#"
        CODE="A1"
        case ${CODE} in
            ??) echo "two chars" ;;
            ???) echo "three chars" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["two chars"]));
}

#[test]
fn case_in_loop() {
    let outputs = run_script(r#"
        for F in "a.rs" "b.py" "c.go"; do
            case ${F} in
                *.rs) echo "rust" ;;
                *.py) echo "python" ;;
                *) echo "other" ;;
            esac
        done
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("rust"), "Should find rust");
    assert!(joined.contains("python"), "Should find python");
    assert!(joined.contains("other"), "Should find other");
}

#[test]
fn case_nested_control_flow() {
    let outputs = run_script(r#"
        X="test"
        case ${X} in
            test)
                if true; then
                    echo "nested-if"
                fi
            ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["nested-if"]));
}

#[test]
fn case_with_brace_expansion() {
    let outputs = run_script(r#"
        FILE="code.ts"
        case ${FILE} in
            *.{js,ts}) echo "JavaScript family" ;;
            *.{c,cpp,h}) echo "C family" ;;
            *) echo "Unknown" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["JavaScript family"]));
}

// ============================================================================
// Arithmetic Expression Integration Tests
// ============================================================================

#[test]
fn arithmetic_simple() {
    let outputs = run_script(r#"
        X=$((1 + 2))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["3"]));
}

#[test]
fn arithmetic_with_variables() {
    let outputs = run_script(r#"
        A=10
        B=20
        C=$((A + B))
        echo ${C}
    "#);
    assert!(outputs_contain(&outputs, &["30"]));
}

#[test]
fn arithmetic_complex() {
    let outputs = run_script(r#"
        X=$((2 * (3 + 4) - 5))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["9"]));
}

#[test]
fn arithmetic_negative() {
    let outputs = run_script(r#"
        X=$((-5 + 3))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["-2"]));
}

#[test]
fn arithmetic_modulo() {
    let outputs = run_script(r#"
        X=$((17 % 5))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["2"]));
}

#[test]
fn arithmetic_in_condition() {
    let outputs = run_script(r#"
        A=5
        B=3
        if [[ $((A + B)) -gt 5 ]]; then
            echo "sum is big"
        fi
    "#);
    assert!(outputs_contain(&outputs, &["sum is big"]));
}

#[test]
fn arithmetic_in_loop() {
    let outputs = run_script(r#"
        SUM=0
        for N in 1 2 3 4 5; do
            SUM=$((SUM + N))
        done
        echo ${SUM}
    "#);
    assert!(outputs_contain(&outputs, &["15"]));
}

#[test]
fn arithmetic_division() {
    let outputs = run_script(r#"
        X=$((100 / 4))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["25"]));
}

#[test]
fn arithmetic_precedence() {
    let outputs = run_script(r#"
        X=$((2 + 3 * 4))
        echo ${X}
    "#);
    // Should be 14, not 20
    assert!(outputs_contain(&outputs, &["14"]));
}

#[test]
fn arithmetic_dollar_var() {
    let outputs = run_script(r#"
        N=7
        X=$(($N * 2))
        echo ${X}
    "#);
    assert!(outputs_contain(&outputs, &["14"]));
}

// ============================================================================
// Cross-Feature Tests
// ============================================================================

#[test]
fn cross_case_with_arithmetic() {
    let outputs = run_script(r#"
        N=$((2 + 3))
        case ${N} in
            5) echo "five" ;;
            *) echo "other" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["five"]));
}

#[test]
fn cross_loop_with_arithmetic_and_case() {
    let outputs = run_script(r#"
        for I in 1 2 3; do
            DOUBLE=$((I * 2))
            case ${DOUBLE} in
                2) echo "one doubled" ;;
                4) echo "two doubled" ;;
                6) echo "three doubled" ;;
            esac
        done
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("one doubled"));
    assert!(joined.contains("two doubled"));
    assert!(joined.contains("three doubled"));
}

#[test]
fn cross_condition_with_arithmetic() {
    let outputs = run_script(r#"
        A=10
        B=5
        if [[ $((A - B)) -gt 3 ]]; then
            echo "difference is significant"
        else
            echo "difference is small"
        fi
    "#);
    assert!(outputs_contain(&outputs, &["difference is significant"]));
}

#[test]
fn cross_nested_case_statements() {
    let outputs = run_script(r#"
        CATEGORY="animal"
        TYPE="dog"
        case ${CATEGORY} in
            animal)
                case ${TYPE} in
                    dog) echo "woof" ;;
                    cat) echo "meow" ;;
                esac
            ;;
            plant) echo "photosynthesis" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["woof"]));
}

#[test]
fn cross_conditional_arithmetic() {
    let outputs = run_script(r#"
        X=10
        if [[ $X -gt 5 ]]; then
            Y=$((X * 2))
        else
            Y=$((X / 2))
        fi
        echo ${Y}
    "#);
    assert!(outputs_contain(&outputs, &["20"]));
}

#[test]
fn cross_case_in_if() {
    // Shell-compatible: use [[ ]] for comparisons, compare to "true" string
    let outputs = run_script(r#"
        FLAG=true
        if [[ ${FLAG} == true ]]; then
            case "test.rs" in
                *.rs) echo "rust in if" ;;
            esac
        fi
    "#);
    assert!(outputs_contain(&outputs, &["rust in if"]));
}

// ============================================================================
// String and Variable Edge Cases
// ============================================================================

#[test]
fn var_with_default_unset() {
    let outputs = run_script(r#"
        echo "${UNDEFINED:-fallback}"
    "#);
    assert!(outputs_contain(&outputs, &["fallback"]));
}

#[test]
fn var_with_default_set() {
    let outputs = run_script(r#"
        DEFINED="value"
        echo "${DEFINED:-fallback}"
    "#);
    assert!(outputs_contain(&outputs, &["value"]));
}

#[test]
fn var_with_default_empty() {
    let outputs = run_script(r#"
        EMPTY=""
        echo "${EMPTY:-fallback}"
    "#);
    assert!(outputs_contain(&outputs, &["fallback"]));
}

#[test]
fn var_length() {
    let outputs = run_script(r#"
        MSG="hello"
        echo "${#MSG}"
    "#);
    assert!(outputs_contain(&outputs, &["5"]));
}

#[test]
fn var_length_empty() {
    let outputs = run_script(r#"
        EMPTY=""
        echo "${#EMPTY}"
    "#);
    assert!(outputs_contain(&outputs, &["0"]));
}

// ============================================================================
// Control Flow Edge Cases
// ============================================================================

#[test]
fn while_loop_with_counter() {
    let outputs = run_script(r#"
        I=0
        while [[ $I -lt 3 ]]; do
            echo "iteration ${I}"
            I=$((I + 1))
        done
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("iteration 0"));
    assert!(joined.contains("iteration 1"));
    assert!(joined.contains("iteration 2"));
}

#[test]
fn break_in_loop() {
    let outputs = run_script(r#"
        for I in "1" "2" "3" "4" "5"; do
            if [[ $I > "2" ]]; then
                break
            fi
            echo ${I}
        done
        echo "done"
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("1"), "Should contain 1: {}", joined);
    assert!(joined.contains("2"), "Should contain 2: {}", joined);
    assert!(!joined.contains("3"), "Should not contain 3: {}", joined);
    assert!(joined.contains("done"), "Should contain done: {}", joined);
}

#[test]
fn continue_in_loop() {
    let outputs = run_script(r#"
        for I in "1" "2" "3" "4" "5"; do
            if [[ $I == "3" ]]; then
                continue
            fi
            echo ${I}
        done
    "#);
    let joined = outputs.join("\n");
    assert!(joined.contains("1"), "Should contain 1: {}", joined);
    assert!(joined.contains("2"), "Should contain 2: {}", joined);
    assert!(!joined.contains("\n3\n") && !joined.contains("3\n") && !joined.contains("\n3"), "Should not contain 3 as separate output: {}", joined);
    assert!(joined.contains("4"), "Should contain 4: {}", joined);
    assert!(joined.contains("5"), "Should contain 5: {}", joined);
}

#[test]
fn elif_chain() {
    let outputs = run_script(r#"
        X=2
        if [[ $X == 1 ]]; then
            echo "one"
        elif [[ $X == 2 ]]; then
            echo "two"
        elif [[ $X == 3 ]]; then
            echo "three"
        else
            echo "other"
        fi
    "#);
    assert!(outputs_contain(&outputs, &["two"]));
}

// ============================================================================
// Glob Pattern Tests
// ============================================================================

#[test]
fn glob_asterisk() {
    let outputs = run_script(r#"
        case "hello.txt" in
            *.txt) echo "text file" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["text file"]));
}

#[test]
fn glob_question() {
    let outputs = run_script(r#"
        case "ab" in
            ??) echo "two chars" ;;
            ???) echo "three chars" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["two chars"]));
}

#[test]
fn glob_char_class_range() {
    let outputs = run_script(r#"
        case "5" in
            [0-9]) echo "digit" ;;
            [a-z]) echo "letter" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["digit"]));
}

#[test]
fn glob_char_class_negated() {
    let outputs = run_script(r#"
        case "A" in
            [!a-z]) echo "not lowercase" ;;
            [a-z]) echo "lowercase" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["not lowercase"]));
}

#[test]
fn glob_brace_expansion() {
    let outputs = run_script(r#"
        case "file.go" in
            *.{rs,go,py}) echo "supported lang" ;;
            *) echo "unsupported" ;;
        esac
    "#);
    assert!(outputs_contain(&outputs, &["supported lang"]));
}
