Test helpers and tests for %def (basic calls, error paths, parameter binding), %set, and variable substitution.

Test helpers (test_utils.rs)

// <<test utils>>=
// crates/weaveback-macro/src/evaluator/tests/test_utils.rs

use crate::evaluator::{EvalConfig, Evaluator};
use std::path::Path;

/// Create an EvalConfig whose include path lives inside `temp_dir`.
/// Callers must keep the `TempDir` alive for the test duration;
/// on drop, Rust cleans up the directory automatically.
pub fn config_in_temp_dir(temp_dir: &Path) -> EvalConfig {
    EvalConfig {
        include_paths: vec![temp_dir.to_path_buf()],
        ..Default::default()
    }
}

/// Convenience wrapper: evaluator whose working files stay inside `temp_dir`.
pub fn evaluator_in_temp_dir(temp_dir: &Path) -> Evaluator {
    Evaluator::new(config_in_temp_dir(temp_dir))
}
// @

Basic macro definition and call (test_macros.rs)

// <<test macros>>=
// crates/weaveback-macro/src/evaluator/tests/test_macros.rs

use crate::macro_api::process_string_defaults;

#[test]
fn test_simple_macro_definition() {
    let source = "%def(test_macro, simple text)\n%test_macro()";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(std::str::from_utf8(&result).unwrap().trim(), "simple text");
}

#[test]
fn test_macro_with_parameters() {
    let source = "%def(greet, name, %{Hello, %(name)!%})\n%greet(World)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "Hello, World!"
    );
}

#[test]
fn test_param_substitution() {
    let source = "%def(test, param, wrap %(param) here)\n%test(value)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "wrap value here"
    );
}

#[test]
fn test_param_paren_precedence() {
    let source = "%def(test, p, text with %(p) here)\n%test(hello)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "text with hello here"
    );
}

#[test]
fn test_complex_param_substitution() {
    let source = "%def(format, text, %{\nbefore %(text),\nmiddle %(text))text,\nafter (%(text))\n%})\n%format(hello)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "before hello,\nmiddle hello)text,\nafter (hello)"
    );
}

#[test]
fn test_multiple_params_and_block() {
    let source =
        "%def(test, a, b, c, %{\n1:%(a)\n2:%(b)\n3:%(c)\n%})\n%test(first,\n second,\n third)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "1:first\n2:second\n3:third"
    );
}

#[test]
fn test_mixed_simple_and_block_bodies() {
    let source = "%def(simple, just text)\n%def(with_param, p,text with %(p))\n%def(complex, %{\ntext with,\nmultiple) lines\n%})\n%simple()\n%with_param(hello)\n%complex()";
    let result = process_string_defaults(source).unwrap();
    let result_str = std::str::from_utf8(&result).unwrap();
    assert!(result_str.contains("just text"));
    assert!(result_str.contains("text with hello"));
    assert!(result_str.contains("text with,\nmultiple) lines"));
}

#[test]
fn test_nested_macro_calls() {
    let source =
        "%def(inner, x, %{inner(%(x))%})\n%def(outer, y, %{outer(%inner(%(y)))%})\n%outer(test)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap().trim(),
        "outer(inner(test))"
    );
}

#[test]
fn test_scope_isolation() {
    let source = "%def(m1, x, %(x))\n%def(m2, x, %m1(other_%(x)))\n%m2(value)";
    let result = process_string_defaults(source).unwrap();
    assert_eq!(std::str::from_utf8(&result).unwrap().trim(), "other_value");
}
// @

%def error paths (test_def.rs)

// <<test def>>=
// crates/weaveback-macro/src/evaluator/tests/test_def.rs

use crate::evaluator::{EvalConfig, EvalError, Evaluator};
use crate::macro_api::{process_string, process_string_defaults};

#[test]
fn test_def_macro_other_errors() {
    // Test missing arguments
    let result = process_string_defaults("%def()");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "Expected InvalidUsage error for empty def"
    );

    // Test single argument
    let result = process_string_defaults("%def(foo)");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "Expected InvalidUsage error for def with only name"
    );

    // Test numeric name
    let result = process_string_defaults("%def(123, body)");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "Expected InvalidUsage error for numeric macro name"
    );

    // Test numeric parameter
    let result = process_string_defaults("%def(foo, 123, body)");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "Expected InvalidUsage error for numeric parameter"
    );

    // Test parameter with equals
    let result = process_string_defaults("%def(foo, param=value, body)");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "Expected InvalidUsage error for parameter with equals"
    );
}

#[test]
fn test_def_macro_basic() {
    let result = process_string_defaults("%def(foo, bar) [%foo()]").unwrap();
    assert_eq!(result, b" [bar]");
}

#[test]
fn test_def_macro_with_params() {
    let result = process_string_defaults(
        "%def(greet, name, message, Hello, %(name)! %(message))\n%greet(Alice, Have a nice day)",
    )
    .unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap(),
        "\nAlice! Have a nice day"
    );
}

#[test]
fn test_def_macro_empty_body() {
    let result = process_string_defaults("%def(foo, bar,)\n%foo()").unwrap();
    assert_eq!(std::str::from_utf8(&result).unwrap(), "\n");
}

#[test]
fn test_def_macro_comma_errors() {
    let result = process_string_defaults("%def(foo bar baz, body)");
    assert!(matches!(result, Err(EvalError::InvalidUsage(_))));

    let result = process_string_defaults("%def(, foo, bar)");
    assert!(matches!(result, Err(EvalError::InvalidUsage(_))));

    let result = process_string_defaults("%def(foo,, bar, baz)");
    assert!(matches!(result, Err(EvalError::InvalidUsage(_))));
}

#[test]
fn test_def_macro_with_comments() {
    let result = process_string_defaults(
        "%def(greet, %/* greeting macro %*/\n\
         name, %// person to greet\n\
         msg, %/* message to show %*/\n\
         Hello %(name)! %(msg)\n\
         )\n\
         %greet(Alice, Good morning)",
    )
    .unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap(),
        "\nHello Alice! Good morning\n"
    );
}

#[test]
fn test_def_macro_spaces() {
    let result = process_string_defaults("%def( foo, bar, baz, output)\n%foo(bar, baz)").unwrap();
    assert_eq!(std::str::from_utf8(&result).unwrap(), "\noutput");
}

#[test]
fn test_def_macro_nested() {
    let result = process_string_defaults(
        "%def(bold, text, **%(text)**)
         %def(greet, name, Hello %bold(dear %(name))!)
         %greet(World)",
    )
    .unwrap();
    assert_eq!(
        std::str::from_utf8(&result).unwrap(),
        "\n         \n         Hello **dear World**!"
    );
}

#[test]
fn test_recursion_depth_limit_returns_error() {
    // A directly self-recursive macro must hit MAX_RECURSION_DEPTH and
    // return a Runtime error rather than stack-overflowing the process.
    let result = process_string_defaults("%def(loop, %loop())\n%loop()");
    assert!(
        matches!(result, Err(EvalError::Runtime(_))),
        "expected Runtime error for infinite recursion, got {:?}", result
    );
    let msg = result.unwrap_err().to_string();
    assert!(
        msg.contains("recursion") || msg.contains("depth"),
        "error message should mention recursion depth: {}", msg
    );
}

#[test]
fn test_mutual_recursion_depth_limit() {
    // Mutually recursive macros: %a calls %b, %b calls %a.
    let src = "%def(a, %b())\n%def(b, %a())\n%a()";
    let result = process_string_defaults(src);
    assert!(
        matches!(result, Err(EvalError::Runtime(_))),
        "expected Runtime error for mutual recursion, got {:?}", result
    );
}

#[test]
fn test_redefine_macro_with_different_script_kind() {
    // Redefining a macro with a different ScriptKind (def → rhaidef) should
    // silently replace it; the second definition wins.
    let src = "%def(compute, x, %(x))\n%rhaidef(compute, x, x + \"!\")\n%compute(hello)";
    let result = process_string_defaults(src).unwrap();
    let output = String::from_utf8(result).unwrap();
    // The rhaidef version appends "!" to its argument.
    assert_eq!(output.trim(), "hello!", "rhaidef should shadow earlier %def");
}

#[test]
fn test_param_with_hyphen_is_rejected() {
    // Hyphens lex as Special tokens; `single_ident_param` must reject them.
    let result = process_string_defaults("%def(foo, my-param, body)");
    assert!(
        matches!(result, Err(EvalError::InvalidUsage(_))),
        "expected InvalidUsage for hyphenated param name, got {:?}", result
    );
}

#[test]
fn test_eager_argument_evaluation_order() {
    // Arguments are evaluated eagerly (to strings) before the macro body runs,
    // but argument evaluation occurs INSIDE the callee's new scope frame.
    //
    // Consequence: %set inside an argument mutates the callee's scope, not the
    // caller's.  The caller's scope is unchanged — so `%(counter)` still reads
    // the caller's value (0), not the callee-scoped mutation (1).
    let src =
        "%def(id, x, %(x))\n\
         %set(counter, 0)\n\
         %id(%set(counter, 1))\n\
         %(counter)";
    let mut ev = Evaluator::new(EvalConfig::default());
    let result = process_string(src, None, &mut ev).unwrap();
    let output = String::from_utf8(result).unwrap();
    // counter in the caller's (global) scope is unchanged — still 0.
    assert!(
        output.trim_end().ends_with('0'),
        "expected counter=0 (caller scope unchanged), got: {:?}", output
    );
}

#[test]
fn test_arguments_evaluated_before_body() {
    // Verify strictness: argument expressions are fully expanded to strings
    // before the macro body executes.
    let src =
        "%def(loud, x, %(x)!)\n\
         %def(join, a, b, %(a)%(b))\n\
         %join(%loud(hi), %loud(there))";
    let result = process_string_defaults(src).unwrap();
    let output = String::from_utf8(result).unwrap();
    assert_eq!(output.trim(), "hi!there!",
        "expected eager evaluation: hi! and there! expanded before join body runs");
}
// @

Variable substitution (test_var.rs)

// <<test var>>=
use crate::macro_api::process_string_defaults;

#[test]
fn test_simple_variable_substitution() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            Hello, %(name)!
        %})
        %greet(World)
        "#,
    )
    .unwrap();

    assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}

#[test]
fn test_nested_variable_substitution() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            Hello, %(name)!
        %})
        %def(greet_twice, name, %{
            %greet(%(name))
            %greet(%(name))
        %})
        %greet_twice(World)
        "#,
    )
    .unwrap();

    assert_eq!(
        String::from_utf8(result).unwrap().trim(),
        "Hello, World!\n        \n            \n            Hello, World!"
    );
}

#[test]
fn test_variable_substitution_with_whitespace() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            Hello, %(name)!
        %})
        %greet(  World  )
        "#,
    )
    .unwrap();

    assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World  !");
}

#[test]
fn test_variable_substitution_with_empty_string() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            Hello, %(name)!
        %})
        %greet()
        "#,
    )
    .unwrap();

    assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, !");
}

#[test]
fn test_variable_substitution_with_multiple_arguments() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, greeting, %{
            %(greeting), %(name)!
        %})
        %greet(World, Hello)
        "#,
    )
    .unwrap();

    assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}

#[test]
fn test_variable_substitution_with_macro_in_argument() {
    let result = process_string_defaults(
        r#"
        %def(get_name, %{
            World
        %})
        %def(greet, name, %{
            Hello, %(name)!
        %})
        %greet(%get_name())
        "#,
    )
    .unwrap();

    assert_eq!(
        String::from_utf8(result).unwrap().trim(),
        "Hello, \n            World\n        !"
    );
}

#[test]
fn test_variable_substitution_with_conditional_logic() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            %if(%(name), %{
                Hello, %(name)!
            %}, %{
                Hello, stranger!
            %})
        %})
        %greet(World)
        "#,
    )
    .unwrap();

    assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}

#[test]
fn test_variable_substitution_with_conditional_logic_empty() {
    let result = process_string_defaults(
        r#"
        %def(greet, name, %{
            %if(%(name), %{
                Hello, %(name)!
            %}, %{
                Hello, stranger!
            %})
        %})
        %greet()
        "#,
    )
    .unwrap();

    assert_eq!(
        String::from_utf8(result).unwrap().trim(),
        "Hello, stranger!"
    );
}
// @

%set (test_set.rs)

// <<test set>>=
#[cfg(test)]
mod tests {
    use crate::macro_api::process_string_defaults;

    #[test]
    fn test_builtin_set() {
        // The %set builtin should set variable "foo" to "bar".
        // Then, the expression "%(foo)" should expand to "bar".
        let source = "%set(foo, bar)%(foo)";
        let result =
            process_string_defaults(source).expect("Failed to process string with %set builtin");
        let output = String::from_utf8(result).expect("Output was not valid UTF-8");
        assert_eq!(output.trim(), "bar");
    }
}
// @