//! Extra end-to-end cases (operators, CLI combinations, field/record edges).

mod common;

use common::{run_awkrs_file, run_awkrs_stdin, run_awkrs_stdin_args, run_awkrs_stdin_args_env};
use std::ffi::OsString;
use std::fs;
use std::process::Command;

#[test]
fn logical_and_short_circuit_skips_rhs_division_by_zero() {
    let (c, o, e) = run_awkrs_stdin("BEGIN { print (0 && 1/0) }", "");
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(o, "0\n");
}

#[test]
fn logical_or_short_circuit_skips_rhs_division_by_zero() {
    let (c, o, e) = run_awkrs_stdin("BEGIN { print (1 || 1/0) }", "");
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(o, "1\n");
}

#[test]
fn gsub_returns_substitution_count_on_record() {
    let (c, o, _) = run_awkrs_stdin(r#"{ print gsub("o", "x") }"#, "foo\n");
    assert_eq!(c, 0);
    assert_eq!(o, "2\n");
}

#[test]
fn sub_returns_at_most_one_on_record() {
    let (c, o, _) = run_awkrs_stdin(r#"{ print sub("o", "x") }"#, "foo\n");
    assert_eq!(c, 0);
    assert_eq!(o, "1\n");
}

#[test]
fn length_empty_string_is_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print length("") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn numeric_lt_false_when_comparing_zero_to_negative() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print (0 < -1) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn posix_exponentiation_caret_star_star_right_assoc() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print 2^3, 2**3, 2^3^2, -2^2 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "8 8 512 -4\n");
}

#[test]
fn getline_expr_compares_return_value() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { if ((getline x) > 0) print x }"#, "hello\n");
    assert_eq!(c, 0);
    assert_eq!(o, "hello\n");
}

#[test]
fn pipe_getline_reads_from_sh_c_pipeline() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { "echo hi" | getline x; print x }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "hi\n");
}

#[test]
fn gawk_regexp_constant_typeof_and_match() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN { r = @/[0-9]+/; print typeof(r), ("a1b" ~ r) }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "regexp 1\n");
}

#[test]
fn printf_group_flag_inserts_separators() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { printf "%'d\n", 1234567 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1,234,567\n");
}

#[test]
fn substr_start_beyond_string_yields_empty() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print substr("hi", 99) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "\n");
}

#[test]
fn substr_start_zero_clamped_like_gawk() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { print substr("hello", 0, 3) }"#, "");
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(o, "hel\n");
}

#[test]
fn substr_large_negative_start_clamped_like_gawk() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { print substr("hello", -10, 3) }"#, "");
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(o, "hel\n");
}

#[test]
fn print_whole_array_scalar_is_fatal() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { a[1] = 1; print a }"#, "");
    assert_ne!(c, 0, "expected nonzero exit, out={o:?} stderr={e:?}");
    assert!(
        e.contains("attempt to use an array in a scalar context"),
        "stderr={e:?}"
    );
}

#[test]
fn print_concat_array_scalar_is_fatal() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { a[1] = 1; print "x=" a "." }"#, "");
    assert_ne!(c, 0, "expected nonzero exit, out={o:?} stderr={e:?}");
    assert!(
        e.contains("attempt to use an array in a scalar context"),
        "stderr={e:?}"
    );
}

#[test]
fn record_pattern_regex_may_start_compound_expression() {
    let (c, o, _) = run_awkrs_stdin(r#"/foo/ && NR > 1 { print $0 }"#, "foo\nfoo\nbar\n");
    assert_eq!(c, 0);
    assert_eq!(o, "foo\n");
}

#[test]
fn negative_field_access_is_fatal_like_gawk() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { print $(-1) }"#, "");
    assert_ne!(c, 0, "out={o:?}");
    assert!(
        e.contains("attempt to access field number -1"),
        "stderr={e:?}"
    );
}

#[test]
fn nf_negative_assignment_is_fatal_like_gawk() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { NF = -1 }"#, "");
    assert_ne!(c, 0, "out={o:?}");
    assert!(e.contains("NF set to negative value"), "stderr={e:?}");
}

#[test]
fn whole_array_in_addition_is_fatal() {
    let (c, _, e) = run_awkrs_stdin(r#"BEGIN { a[1]=1; print a + 1 }"#, "");
    assert_ne!(c, 0);
    assert!(
        e.contains("attempt to use an array in a scalar context"),
        "stderr={e:?}"
    );
}

#[test]
fn whole_array_in_equality_compare_is_fatal() {
    let (c, _, e) = run_awkrs_stdin(r#"BEGIN { a[1]=1; print (a == 0) }"#, "");
    assert_ne!(c, 0);
    assert!(
        e.contains("attempt to use an array in a scalar context"),
        "stderr={e:?}"
    );
}

#[test]
fn whole_array_in_if_condition_is_fatal() {
    let (c, _, e) = run_awkrs_stdin(r#"BEGIN { a[1]=1; if (a) print 1 }"#, "");
    assert_ne!(c, 0);
    assert!(
        e.contains("attempt to use an array in a scalar context"),
        "stderr={e:?}"
    );
}

#[test]
fn division_by_zero_is_fatal_like_gawk() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { print 1/0 }"#, "");
    assert_ne!(c, 0, "stderr={e:?}");
    assert!(
        e.contains("division by zero attempted"),
        "stderr={e:?} stdout={o:?}"
    );
}

#[test]
fn index_miss_returns_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print index("abc", "z") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn compound_subtract_assign() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { x = 5; x -= 2; print x }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "3\n");
}

#[test]
fn compound_mod_assign() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { x = 17; x %= 5; print x }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "2\n");
}

#[test]
fn compound_add_assign_string_coerces() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { x = \"3\"; x += 2; print x }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "5\n");
}

#[test]
fn print_nr_zero_records_still_prints_begin() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print \"start\" } { } END { print \"end\" }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "start\nend\n");
}

#[test]
fn empty_line_produces_empty_field_one() {
    let (c, o, _) = run_awkrs_stdin("{ print NF, length($0) }", "\n");
    assert_eq!(c, 0);
    assert_eq!(o, "0 0\n");
}

#[test]
fn multi_char_field_separator_flag() {
    let (c, o, _) = run_awkrs_stdin_args(["-F", "::"], "{ print $2 }", "a::b::c\n");
    assert_eq!(c, 0);
    assert_eq!(o, "b\n");
}

#[test]
fn two_v_flags_combine() {
    let (c, o, _) = run_awkrs_stdin_args(["-v", "a=1", "-v", "b=2"], "BEGIN { print a + b }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "3\n");
}

#[test]
fn include_prepends_script() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let inc = dir.join(format!("awkrs_inc_{id}.awk"));
    fs::write(&inc, "function hi() { return \"hi\" }\n").expect("write include");
    let bin = env!("CARGO_BIN_EXE_awkrs");
    let out = Command::new(bin)
        .args([
            "-i",
            inc.to_str().expect("utf8"),
            "-e",
            "BEGIN { print hi() }",
        ])
        .output()
        .expect("spawn");
    let _ = fs::remove_file(&inc);
    assert_eq!(
        out.status.code(),
        Some(0),
        "stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&out.stdout), "hi\n");
}

#[test]
fn for_loop_sum_indices() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { s=0; for (i=1;i<=5;i=i+1) s+=i; print s }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "15\n");
}

#[test]
fn jit_array_field_add_const_fused_opcode() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ a[$1] += 1 } END { print a[\"x\"] + 0 }",
        "x\nx\nx\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "3\n");
}

#[test]
fn awk_style_truthiness_zero_and_empty() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { print (0 ? 1 : 0), (\"\" ? 1 : 0), (\"0\" ? 1 : 0) }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "0 0 0\n");
}

#[test]
fn postincrement_style_via_assignment() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { x=1; x=x+1; print x }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "2\n");
}

#[test]
fn relop_numeric_when_both_operands_are_numeric_strings() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (\"10\" < \"2\"), (10 < 2) }", "");
    assert_eq!(c, 0);
    // String constants compare lexicographically; numeric constants compare as numbers (matches gawk).
    assert_eq!(o, "1 0\n");
}

#[test]
fn regexp_bracket_class() {
    let (c, o, _) = run_awkrs_stdin(r#"/[aeiou]+/ { print "vowels" }"#, "queue\n");
    assert_eq!(c, 0);
    assert_eq!(o, "vowels\n");
}

#[test]
fn field_reference_negative_is_fatal_like_gawk() {
    let (c, o, e) = run_awkrs_stdin("{ print $-1 }", "a b\n");
    assert_ne!(c, 0, "out={o:?} stderr={e:?}");
    assert!(
        e.contains("attempt to access field number -1"),
        "stderr={e:?}"
    );
}

#[test]
fn printf_width_d() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { printf "%05d\n", 7 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "00007\n");
}

#[test]
fn begin_end_order_with_multiple_rules() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print 1 } { } END { print 2 }", "x\n");
    assert_eq!(c, 0);
    assert_eq!(o, "1\n2\n");
}

#[test]
fn slurp_file_multiline_sum() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let path = dir.join(format!("awkrs_extra_sum_{id}.txt"));
    fs::write(&path, "10\n20\n30\n").expect("write");
    let (c, o, _) = run_awkrs_file("{ s += $1 } END { print s }", &path);
    let _ = fs::remove_file(&path);
    assert_eq!(c, 0);
    assert_eq!(o, "60\n");
}

#[test]
fn concat_numbers_coerces_to_string() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print 1 2 3 }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "123\n");
}

#[test]
fn equality_string_vs_number() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (\"00\" == 0) }", "");
    assert_eq!(c, 0);
    // Literal "00" is not a numeric string for == against a number; falls through to string cmp vs "0" (gawk: 0).
    assert_eq!(o, "0\n");
}

#[test]
fn line_continuation_in_string_not_expected() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print "ab" }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "ab\n");
}

#[test]
fn subexpr_parentheses_precedence() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (2+3)*4 }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "20\n");
}

#[test]
fn unary_plus_on_string() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print +\"42\" }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "42\n");
}

#[test]
fn array_delete_then_missing_is_empty() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { a[\"x\"]=1; delete a[\"x\"]; print a[\"x\"] }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "\n");
}

#[test]
fn split_empty_fs_yields_char_fields() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { n = split(\"ab\", t, \"\"); print n, t[1], t[2] }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "2 a b\n");
}

#[test]
fn print_ofmt_default_for_float() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print 1.5 }", "");
    assert_eq!(c, 0);
    assert!(o.starts_with("1.5") || o.starts_with("1.500"), "o={o:?}");
}

#[test]
fn multiple_print_args_empty_field() {
    let (c, o, _) = run_awkrs_stdin("{ print $1, $2 }", "onlyone\n");
    assert_eq!(c, 0);
    assert_eq!(o, "onlyone \n");
}

#[test]
fn beginfile_runs_before_records() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let path = dir.join(format!("awkrs_bf_{id}.txt"));
    fs::write(&path, "z\n").expect("write");
    let bin = env!("CARGO_BIN_EXE_awkrs");
    let out = Command::new(bin)
        .arg("BEGINFILE { print \"bf\" } { print $1 } ENDFILE { print \"ef\" }")
        .arg(&path)
        .output()
        .expect("spawn");
    let _ = fs::remove_file(&path);
    assert_eq!(
        out.status.code(),
        Some(0),
        "stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&out.stdout), "bf\nz\nef\n");
}

#[test]
fn stderr_redirect_unknown_function() {
    let (c, _, e) = run_awkrs_stdin("{ zz() }", "a\n");
    assert_ne!(c, 0);
    assert!(!e.is_empty());
}

#[test]
fn or_operator_both_false() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (0 || 0) }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn and_operator_both_true() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (1 && 1) }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "1\n");
}

#[test]
fn xor_builtin_bitwise() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print xor(1,2) }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "3\n");
}

#[test]
fn strtonum_hex_octal_decimal() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { print strtonum(\"0x1F\"), strtonum(\"017\"), strtonum(\"42\") }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "31 15 42\n");
}

#[test]
fn bitwise_shift_and_compl_zero() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { print and(3,1), or(2,1), lshift(1,4), rshift(16,2) }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "1 3 16 4\n");
}

#[test]
fn asort_by_value_reindexes() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { a[\"x\"]=3; a[\"y\"]=1; n=asort(a); print n, a[\"1\"], a[\"2\"] }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "2 1 3\n");
}

#[test]
fn asorti_sorts_keys() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { a[\"b\"]=1; a[\"a\"]=2; asorti(a); print a[\"1\"], a[\"2\"] }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "a b\n");
}

#[test]
fn switch_case_first_match() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { switch (2) { case 1: print \"a\"; break; case 2: print \"b\"; break; default: print \"c\" } }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "b\n");
}

#[test]
fn switch_case_regex_label() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { x = \"foo\"; switch (x) { case /foo/: print \"ok\"; break } }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "ok\n");
}

#[test]
fn switch_break_does_not_break_enclosing_while() {
    let (c, o, _) = run_awkrs_stdin(
        "BEGIN { i = 0; while (i < 1) { i++; switch (1) { case 1: break } print \"after\" } }",
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "after\n");
}

#[test]
fn duplicate_field_sum() {
    let (c, o, _) = run_awkrs_stdin("{ t += $1 } END { print t }", "1\n2\n3\n");
    assert_eq!(c, 0);
    assert_eq!(o, "6\n");
}

#[test]
fn gensub_not_builtin_errors() {
    let (c, _, _) = run_awkrs_stdin("BEGIN { gensub() }", "");
    assert_ne!(c, 0);
}

#[test]
fn record_separator_default_newline() {
    let (c, o, _) = run_awkrs_stdin("{ print $1 }", "p q\nr s\n");
    assert_eq!(c, 0);
    assert_eq!(o, "p\nr\n");
}

#[test]
fn assignment_expression_value() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print (x = 5) }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "5\n");
}

#[test]
fn empty_program_file_only_end() {
    let (c, o, _) = run_awkrs_stdin("END { print NR }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn compare_nan_never_equals_itself_awk_semantics() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { n = sqrt(-1); print (n == n) }", "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

// ── FILENAME, match() globals, sub/gsub target, system(), mawk -W ─────────

#[test]
fn filename_stdin_is_dash() {
    let (c, o, _) = run_awkrs_stdin("{ print FILENAME }", "a\n");
    assert_eq!(c, 0);
    assert_eq!(o, "-\n");
}

#[test]
fn filename_reflects_input_file_basename() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let base = format!("awkrs_filename_{id}.txt");
    let path = dir.join(&base);
    fs::write(&path, "x\n").expect("write temp");
    let (c, o, e) = run_awkrs_file("{ print FILENAME }", &path);
    let _ = fs::remove_file(&path);
    assert_eq!(c, 0, "stderr={e:?}");
    let printed = o.trim_end();
    assert!(
        printed.ends_with(&base),
        "expected path ending with {base:?}, got {printed:?}"
    );
}

#[test]
fn match_builtin_sets_rstart_rlength() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN { match("foo123bar", "[0-9]+"); print RSTART, RLENGTH }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "4 3\n");
}

#[test]
fn match_no_match_sets_rstart_zero_rlength_negative() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { match("abc", "z"); print RSTART, RLENGTH }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0 -1\n");
}

#[test]
fn system_reports_nonzero_exit_status() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print system("exit 4") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o.trim(), "4");
}

#[test]
fn sub_third_arg_replaces_in_scalar_variable() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { x = "hello"; sub("l", "L", x); print x }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "heLlo\n");
}

#[test]
fn gsub_third_arg_replaces_all_in_scalar_variable() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { x = "ll"; gsub("l", "L", x); print x }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "LL\n");
}

#[test]
fn mawk_w_version_matches_dash_capital_v() {
    let w = Command::new(env!("CARGO_BIN_EXE_awkrs"))
        .args(["-W", "version"])
        .output()
        .expect("spawn awkrs -W version");
    assert!(
        w.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&w.stderr)
    );
    let dash_v = Command::new(env!("CARGO_BIN_EXE_awkrs"))
        .arg("-V")
        .output()
        .expect("spawn awkrs -V");
    assert_eq!(w.stdout, dash_v.stdout);
}

// ── typeof(), regexp alternation, empty OFS ─────────────────────────────────

#[test]
fn typeof_builtin_classifies_numbers_strings_and_uninitialized() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN { print typeof(1), typeof("x"), typeof(x); a[1]=2; print typeof(a[1]) }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "number string uninitialized\nnumber\n");
}

#[test]
fn regexp_pattern_alternation_matches_either_branch() {
    let (c, o, _) = run_awkrs_stdin(r#"/a|b/ { print $0 }"#, "xx\nb\n");
    assert_eq!(c, 0);
    assert_eq!(o, "b\n");
}

#[test]
fn empty_ofs_joins_print_args_without_separator() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { OFS = ""; print 1, 2, 3 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "123\n");
}

// ── close(), ENDFILE, parse errors, IEEE math edges ─────────────────────────

#[test]
fn close_one_way_pipe_returns_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print "q" | "cat"; print close("cat") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "q\n0\n");
}

#[test]
fn getline_from_missing_file_returns_minus_one_and_sets_errno() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let path = dir.join(format!("awkrs_getline_enoent_{id}.txt"));
    assert!(
        !path.exists(),
        "precondition: temp path must not exist: {}",
        path.display()
    );
    let prog = format!(
        r#"BEGIN {{ print (getline x < "{}"), (length(ERRNO) > 0) }}"#,
        path.display()
    );
    let (c, o, _) = run_awkrs_stdin(&prog, "");
    assert_eq!(c, 0);
    assert_eq!(o, "-1 1\n");
}

#[test]
fn close_returns_zero_after_getline_from_named_file() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let path = dir.join(format!("awkrs_close_after_getline_{id}.txt"));
    fs::write(&path, "row\n").expect("temp");
    let prog = format!(
        r#"BEGIN {{ print (getline x < "{}"), x; print close("{}") }}"#,
        path.display(),
        path.display()
    );
    let (c, o, _) = run_awkrs_stdin(&prog, "");
    let _ = fs::remove_file(&path);
    assert_eq!(c, 0);
    assert_eq!(o, "1 row\n0\n");
}

#[test]
fn gensub_ampersand_substitutes_entire_match() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print gensub(/a+/, "[&]", "g", "xaay") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "x[aa]y\n");
}

#[test]
fn split_accepts_slash_regex_field_separator() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN {
            n = split("a1b2c", t, /[0-9]/)
            print n
            for (i = 1; i <= n; i++) print t[i]
        }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "3\na\nb\nc\n");
}

#[test]
fn split_reinitializes_destination_array_removing_old_keys() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN {
            a[9] = 1
            n = split("x:y", a, ":")
            print n, a[1], a[2], (9 in a)
        }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "2 x y 0\n");
}

#[test]
fn for_c_style_infinite_loop_breaks_once() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { for (;;) { break } print "done" }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "done\n");
}

#[test]
fn sprintf_percent_s_respects_minimum_width() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { printf ">%5s<\n", "ab" }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, ">   ab<\n");
}

#[test]
fn int_of_missing_field_is_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"{ print int($2) }"#, "a\n");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn getline_from_dev_null_returns_zero_immediately() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print (getline x < "/dev/null") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn system_empty_command_reports_success() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print system("") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o.trim(), "0");
}

#[test]
fn multichar_ofs_used_between_print_arguments() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { OFS = "::"; print 1, 2 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1::2\n");
}

#[test]
fn assigning_dollar_one_to_empty_string_keeps_nf() {
    let (c, o, _) = run_awkrs_stdin(r#"{ $1 = ""; print NF, $2 }"#, "a b\n");
    assert_eq!(c, 0);
    assert_eq!(o, "2 b\n");
}

#[test]
fn zero_raised_to_zero_is_one() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print 0^0, 1^0 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1 1\n");
}

#[test]
fn chained_assignment_sets_all_lvalues() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { x = y = 3; print x, y }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "3 3\n");
}

#[test]
fn string_concatenation_with_empty_middle_term() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print "a" "" "b" }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "ab\n");
}

#[test]
fn print_numeric_respects_ofmt() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { OFMT = "%.2f"; print 1.234 }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1.23\n");
}

#[test]
fn string_concatenation_uses_convfmt_for_number_coercion() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { CONVFMT = "%.1f"; x = 1.23; print "" x }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1.2\n");
}

#[test]
fn gensub_numeric_how_replaces_single_occurrence() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print gensub(/a/, "A", 1, "aba") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "Aba\n");
}

#[test]
fn regexp_match_operator_accepts_pattern_in_variable() {
    let (c, o, _) = run_awkrs_stdin(r#"{ pat = "[0-9]+"; print ($0 ~ pat) }"#, "abc\n42\n");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n1\n");
}

#[test]
fn modulo_with_negative_dividend_matches_awk_semantics() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print (-7 % 3), (7 % -3) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "-1 1\n");
}

#[test]
fn multichar_ors_between_print_records() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { ORS = "|\n" } { print $1 }"#, "a\nb\n");
    assert_eq!(c, 0);
    assert_eq!(o, "a|\nb|\n");
}

#[test]
fn postfix_increment_uninitialized_scalar_starts_at_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print x++ }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn gensub_numeric_zero_replaces_all_occurrences() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print gensub(/a/, "A", 0, "aba") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "AbA\n");
}

#[test]
fn intdiv0_truncates_toward_zero_and_zero_divisor_yields_zero() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN { print intdiv0(7, 3), intdiv0(-7, 3), intdiv0(7, 0) }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "2 -2 0\n");
}

#[test]
fn decrement_nf_drops_trailing_field_and_rebuilds_dollar_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"{ print NF; NF--; print NF, $0 }"#, "a b c\n");
    assert_eq!(c, 0);
    assert_eq!(o, "3\n2 a b\n");
}

#[test]
fn length_of_uninitialized_scalar_is_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print length(s) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn logical_and_empty_string_is_falsy() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print ("" && 1), ("x" && 1) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0 1\n");
}

#[test]
fn compl_bitwise_not_of_one() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print compl(1) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "-2\n");
}

#[test]
fn gsub_with_empty_replacement_removes_matches_in_scalar() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN { s = "aba"; n = gsub(/a/, "", s); print n, s }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "2 b\n");
}

#[test]
fn for_in_delete_removes_all_array_elements() {
    let (c, o, _) = run_awkrs_stdin(
        r#"BEGIN {
            a[1] = 1
            a[2] = 2
            for (i in a) delete a[i]
            print length(a)
        }"#,
        "",
    );
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn isarray_distinguishes_arrays_from_scalars() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { a[1] = 1; print isarray(a), isarray(x) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1 0\n");
}

#[test]
fn sub_returns_zero_when_pattern_does_not_match_third_arg() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print sub(/z/, "Z", "abc") }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0\n");
}

#[test]
fn strftime_third_argument_selects_utc() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print strftime("%H", 0, 1) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "00\n");
}

#[test]
fn print_empty_string_still_writes_output_record_separator() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print "" }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "\n");
}

#[test]
fn argv_zero_names_the_awk_binary() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print (index(ARGV[0], "awkrs") > 0) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o.trim(), "1");
}

#[test]
fn bitwise_xor_same_value_and_or_and_with_zero() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print xor(5, 5), or(0, 0), and(3, 0) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "0 0 0\n");
}

#[test]
fn lshift_and_rshift_by_zero_are_identity() {
    let (c, o, _) = run_awkrs_stdin(r#"BEGIN { print lshift(1, 0), rshift(8, 0) }"#, "");
    assert_eq!(c, 0);
    assert_eq!(o, "1 8\n");
}

#[test]
fn endfile_runs_once_per_input_file() {
    let dir = std::env::temp_dir();
    let id = std::process::id();
    let f1 = dir.join(format!("awkrs_endfile_a_{id}.txt"));
    let f2 = dir.join(format!("awkrs_endfile_b_{id}.txt"));
    fs::write(&f1, "x\n").expect("temp");
    fs::write(&f2, "y\n").expect("temp");
    let out = Command::new(env!("CARGO_BIN_EXE_awkrs"))
        .arg(r#"ENDFILE { print "E" }"#)
        .arg(&f1)
        .arg(&f2)
        .output()
        .expect("spawn awkrs two files");
    let _ = fs::remove_file(&f1);
    let _ = fs::remove_file(&f2);
    assert_eq!(
        out.status.code(),
        Some(0),
        "stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&out.stdout), "E\nE\n");
}

#[test]
fn unclosed_brace_program_exits_nonzero_with_parse_error() {
    let out = Command::new(env!("CARGO_BIN_EXE_awkrs"))
        .arg("{")
        .output()
        .expect("spawn awkrs invalid program");
    assert_ne!(out.status.code(), Some(0));
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("parse error"),
        "expected parse error on stderr, got: {stderr:?}"
    );
}

#[test]
fn log_zero_is_negative_infinity() {
    let (c, o, _) = run_awkrs_stdin("BEGIN { print log(0) }", "");
    assert_eq!(c, 0);
    let t = o.trim();
    assert!(t.eq_ignore_ascii_case("-inf"), "expected -inf, got {o:?}");
}

#[test]
fn sqrt_negative_one_is_nan() {
    let (c, o, e) = run_awkrs_stdin("BEGIN { print sqrt(-1) }", "");
    assert_eq!(c, 0);
    let t = o.trim();
    assert!(t.eq_ignore_ascii_case("nan"), "expected NaN, got {o:?}");
    assert!(
        e.contains("sqrt: received negative argument"),
        "gawk warns on stderr even without LINT; stderr={e:?}"
    );
}

#[test]
fn log_negative_one_warns_and_yields_nan() {
    let (c, o, e) = run_awkrs_stdin("BEGIN { print log(-1) }", "");
    assert_eq!(c, 0);
    assert!(o.trim().eq_ignore_ascii_case("nan"), "stdout={o:?}");
    assert!(
        e.contains("log: received negative argument"),
        "stderr={e:?}"
    );
}

// ── JIT integration tests ───────────────────────────────────────────────

#[test]
fn jit_print_field_stdout() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ print $1 }",
        "hello\nworld\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "hello\nworld\n");
}

#[test]
fn jit_print_two_fields() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ print $1, $2 }",
        "a b\nc d\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "a b\nc d\n");
}

#[test]
fn jit_bare_print() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ print }",
        "line1\nline2\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "line1\nline2\n");
}

#[test]
fn jit_match_regexp_pattern() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "/yes/ { print $0 }",
        "no\nyes\nmaybe\nyes please\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "yes\nyes please\n");
}

#[test]
fn jit_next_skips_rule() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "$1 == \"skip\" { next } { print $0 }",
        "keep\nskip\nalso keep\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "keep\nalso keep\n");
}

#[test]
fn jit_exit_default() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ print; exit }",
        "first\nsecond\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "first\n");
}

#[test]
fn jit_exit_with_code() {
    let (c, _o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { exit 42 }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 42, "stderr: {e}");
}

#[test]
fn jit_array_count_pattern() {
    // Classic: count[$1]++ then print
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ a[$1] += 1 } END { print a[\"x\"] + 0, a[\"y\"] + 0 }",
        "x\ny\nx\nx\ny\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "3 2\n");
}

#[test]
fn jit_split_explicit_fs() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { n = split(\"a,b\", arr, \",\"); print n, arr[1], arr[2] }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2 a b\n");
}

#[test]
fn jit_split_uses_fs_variable() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { FS = \",\"; n = split(\"a,b\", arr); print n, arr[1], arr[2] }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2 a b\n");
}

#[test]
fn jit_patsplit_fpat() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { FPAT=\"[^,]+\"; n = patsplit(\"a,b\", arr); print n, arr[1], arr[2] }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2 a b\n");
}

#[test]
fn jit_match_builtin_rstart() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { print match("foo123bar", "[0-9]+"), RSTART, RLENGTH }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "4 4 3");
}

#[test]
fn jit_print_redirect_overwrite() {
    let dir = std::env::temp_dir();
    let path = dir.join(format!("awkrs_jit_redir_{}.txt", std::process::id()));
    let _ = std::fs::remove_file(&path);
    let p = path.to_string_lossy().replace('\\', "/");
    let prog = format!(r#"BEGIN {{ print "hello" > "{p}" }}"#);
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        &prog,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert!(o.is_empty());
    let contents = std::fs::read_to_string(&path).expect("read redirected output");
    assert_eq!(contents, "hello\n");
    let _ = std::fs::remove_file(&path);
}

#[test]
fn jit_sum_fields_loop() {
    // for (i=1; i<=NF; i++) sum += $i
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ s=0; for(i=1;i<=NF;i++) s+=$i; print s }",
        "1 2 3\n10 20\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "6\n30\n");
}

#[test]
fn jit_mixed_string_slot_preinc() {
    // Mixed chunk: string literal + `++` on slot must coerce "5" → 5 → 6 (not raw f64 add on NaN bits).
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { s=\"5\"; s++; print s }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "6");
}

#[test]
fn jit_mixed_field_assign_concat() {
    // `$2 = $1 "x"` must store a real string via MIXED_SET_FIELD (not Value::Num bits as text).
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ $2 = $1 \"x\"; print $2 }",
        "a\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "ax");
}

#[test]
fn jit_multidim_array_subscript() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "BEGIN { a[1,2]=42; print a[1,2] }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "42");
}

#[test]
fn jit_typeof_expressions_and_slot() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { x=1; print typeof(3), typeof("x"), typeof(x) }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "number string number");
}

#[test]
fn jit_typeof_field_unassigned() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"{ print typeof($2) }"#,
        "a\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "uninitialized");
}

#[test]
fn jit_whitelisted_builtin_sqrt() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { print sqrt(9) }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "3");
}

#[test]
fn jit_sprintf_builtin() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { print sprintf("%d", 42) }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "42");
}

#[test]
fn jit_printf_statement_stdout() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { printf "%d\n", 42 }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o.trim(), "42");
}

#[test]
fn jit_return_from_function() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "function double(x) { return x * 2 } BEGIN { print double(21) }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "42\n");
}

#[test]
fn jit_gsub_on_record() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"{ print gsub("o", "x") }"#,
        "foo\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2\n");
}

#[test]
fn jit_sub_on_record() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"{ print sub("o", "x") }"#,
        "foo\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "1\n");
}

#[test]
fn jit_nested_user_function_calls() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "function g() { return 1 } function f() { return g() + 1 } BEGIN { print f() }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2\n");
}

#[test]
fn jit_gsub_third_arg_scalar() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        r#"BEGIN { s = "foo"; print gsub("o", "x", s), s }"#,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "2 fxx\n");
}

#[test]
fn jit_for_in_count_keys() {
    // Uses fused ArrayFieldAddConst for `a[$1] += 1` (correct string-key path),
    // then ForIn to count keys.
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ a[$1] += 1 } END { n=0; for (k in a) n++; print n }",
        "x\ny\nz\nx\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "3\n");
}

#[test]
fn jit_for_in_sum_values() {
    // Uses fused ArrayFieldAddConst for array population,
    // then ForIn + fused ArrayFieldAddConst to sum values.
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "{ a[$1] += $2 } END { s=0; for (k in a) s += a[k]; print s }",
        "x 10\ny 20\nz 30\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "60\n");
}

#[test]
fn jit_asort_returns_count() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "END { a[1]=30; a[2]=10; a[3]=20; print asort(a) }",
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "3\n");
}

#[test]
fn jit_getline_primary_reads_next_line() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "NR==1 { getline; print $0 }",
        "a\nb\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "b\n");
}

#[test]
fn jit_getline_into_var() {
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        "NR==1 { getline x; print x }",
        "a\nb\n",
        std::iter::empty::<(OsString, OsString)>(),
    );
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "b\n");
}

#[test]
fn jit_getline_from_file() {
    let dir = std::env::temp_dir();
    let path = dir.join(format!("awkrs_jit_getline_{}.txt", std::process::id()));
    std::fs::write(&path, "fromfile\n").unwrap();
    let path_str = path.to_string_lossy();
    let prog = format!(
        "BEGIN {{ getline x < \"{}\"; print x }}",
        path_str.replace('\\', "\\\\")
    );
    let (c, o, e) = run_awkrs_stdin_args_env(
        std::iter::empty::<&str>(),
        &prog,
        "",
        std::iter::empty::<(OsString, OsString)>(),
    );
    let _ = std::fs::remove_file(&path);
    assert_eq!(c, 0, "stderr: {e}");
    assert_eq!(o, "fromfile\n");
}

#[test]
fn gawk_style_paren_list_in_and_literals_printf_substr_index() {
    let (c, o, e) = run_awkrs_stdin(
        r#"BEGIN {
  a[1, 2] = 1
  print ((1,2) in a), 0x10, 010, 01238
  printf "%e\n", 1234.5
  printf "[%c]\n", "Z"
  print substr("hi", -1, 5)
  print index("hello", "")
}"#,
        "",
    );
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(
        o,
        "1 16 8 1238\n\
1.234500e+03\n\
[Z]\n\
hi\n\
1\n"
    );
}

#[test]
fn print_paren_list_emits_multiple_fields() {
    let (c, o, e) = run_awkrs_stdin(r#"BEGIN { OFS=","; print (1, 2) }"#, "");
    assert_eq!(c, 0, "stderr={e:?}");
    assert_eq!(o, "1,2\n");
}
