use std::{fs, path::Path, process::Command};

#[test]
fn option_result_fixture_emits_llvm_aggregate_shape() {
    let output = run_glagol(["../examples/option-result.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected option/result fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define { i1, i32 } @maybe_value()")
            && stdout.contains("define { i1, i32 } @maybe_empty()")
            && stdout.contains("define { i1, i64 } @maybe_wide_value()")
            && stdout.contains("define { i1, i64 } @maybe_wide_empty()")
            && stdout.contains("define { i1, double } @maybe_float_value()")
            && stdout.contains("define { i1, double } @maybe_float_empty()")
            && stdout.contains("define { i1, ptr } @maybe_string_value()")
            && stdout.contains("define { i1, ptr } @maybe_string_empty()")
            && stdout.contains("define { i1, i32 } @result_ok()")
            && stdout.contains("define { i1, i32 } @result_err()")
            && stdout.contains("insertvalue { i1, i32 } undef, i1 1, 0")
            && stdout.contains("insertvalue { i1, i32 } undef, i1 0, 0")
            && stdout.contains("insertvalue { i1, i64 } undef, i1 1, 0")
            && stdout.contains("insertvalue { i1, i64 } undef, i1 0, 0")
            && stdout.contains("insertvalue { i1, double } undef, i1 1, 0")
            && stdout.contains("insertvalue { i1, double } %")
            && stdout.contains("double 0.0")
            && stdout.contains("insertvalue { i1, ptr } undef, i1 1, 0")
            && stdout.contains("insertvalue { i1, ptr } undef, i1 0, 0")
            && stdout.contains("insertvalue { i1, i32 } %")
            && stdout.contains("insertvalue { i1, i64 } %")
            && stdout.contains("insertvalue { i1, ptr } %")
            && stdout.contains("ret { i1, i32 } %"),
        "LLVM output did not contain expected option/result aggregate shape\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("define ptr @maybe_value")
            && !stdout.contains("define ptr @maybe_empty")
            && !stdout.contains("define ptr @maybe_string_value")
            && !stdout.contains("define ptr @maybe_string_empty")
            && !stdout.contains("define ptr @result_ok")
            && !stdout.contains("define ptr @result_err"),
        "LLVM output used ptr fallback for option/result returns\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_fixture_is_formatter_stable() {
    let expected = fs::read_to_string("../tests/option-result.slo").expect("read fixture");
    let output = run_glagol(["--format", "../tests/option-result.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "formatter rejected option/result fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "formatter output drifted");
    assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/option-result.slo",
    ]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    let surface_stderr = String::from_utf8_lossy(&surface.stderr);
    assert!(
        surface.status.success(),
        "surface lowering rejected option/result fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn maybe_value() -> (option i32)")
            && surface_stdout.contains("fn maybe_wide_value() -> (option i64)")
            && surface_stdout.contains("fn maybe_string_value() -> (option string)")
            && surface_stdout.contains("some i32")
            && surface_stdout.contains("some i64")
            && surface_stdout.contains("some string")
            && surface_stdout.contains("none i32")
            && surface_stdout.contains("none i64")
            && surface_stdout.contains("none string")
            && surface_stdout.contains("ok i32 i32")
            && surface_stdout.contains("err i32 i32"),
        "surface lowering output lost option/result shape\nstdout:\n{}",
        surface_stdout
    );
    assert!(
        surface_stderr.is_empty(),
        "surface lowering wrote stderr:\n{}",
        surface_stderr
    );

    let checked = run_glagol([
        "--inspect-lowering=checked",
        "../examples/option-result.slo",
    ]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    let checked_stderr = String::from_utf8_lossy(&checked.stderr);
    assert!(
        checked.status.success(),
        "checked lowering rejected option/result fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("some : (option i32)")
            && checked_stdout.contains("none : (option i32)")
            && checked_stdout.contains("some : (option i64)")
            && checked_stdout.contains("none : (option i64)")
            && checked_stdout.contains("some : (option string)")
            && checked_stdout.contains("none : (option string)")
            && checked_stdout.contains("ok : (result i32 i32)")
            && checked_stdout.contains("err : (result i32 i32)"),
        "checked lowering output lost typed option/result shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn option_result_flow_fixture_emits_llvm_value_flow_shape() {
    let output = run_glagol(["../examples/option-result-flow.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected option/result flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define i32 @option_score({ i1, i32 } %value)")
            && stdout.contains("define i32 @option_empty_score({ i1, i32 } %value)")
            && stdout.contains("define i32 @option_float_score({ i1, double } %value)")
            && stdout.contains("define i32 @option_float_empty_score({ i1, double } %value)")
            && stdout.contains("define i32 @option_string_score({ i1, ptr } %value)")
            && stdout.contains("define i32 @option_string_empty_score({ i1, ptr } %value)")
            && stdout.contains("define i32 @result_success_score({ i1, i32 } %value)")
            && stdout.contains("define i32 @result_failure_score({ i1, i32 } %value)")
            && stdout.contains("%value.addr = alloca { i1, i32 }")
            && stdout.contains("store { i1, i32 } %")
            && stdout.contains("load { i1, i32 }, ptr %value.addr")
            && stdout.contains("extractvalue { i1, i32 } %")
            && stdout.contains("%value.addr = alloca { i1, double }")
            && stdout.contains("store { i1, double } %")
            && stdout.contains("load { i1, double }, ptr %value.addr")
            && stdout.contains("extractvalue { i1, double } %")
            && stdout.contains("%value.addr = alloca { i1, ptr }")
            && stdout.contains("store { i1, ptr } %")
            && stdout.contains("load { i1, ptr }, ptr %value.addr")
            && stdout.contains("extractvalue { i1, ptr } %")
            && stdout.contains("xor i1 %"),
        "LLVM output did not contain expected option/result value-flow shape\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("define ptr @option_score")
            && !stdout.contains("define ptr @result_success_score"),
        "LLVM output used ptr fallback for option/result params\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_flow_fixture_runs_top_level_tests() {
    let output = run_glagol(["--run-tests", "../examples/option-result-flow.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "test runner rejected option/result flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(
        stdout,
        concat!(
            "test \"option local value flow\" ... ok\n",
            "test \"option call observation\" ... ok\n",
            "test \"option i64 local value flow\" ... ok\n",
            "test \"option i64 call observation\" ... ok\n",
            "test \"option f64 local value flow\" ... ok\n",
            "test \"option f64 call observation\" ... ok\n",
            "test \"option bool local value flow\" ... ok\n",
            "test \"option bool call observation\" ... ok\n",
            "test \"option string local value flow\" ... ok\n",
            "test \"option string call observation\" ... ok\n",
            "test \"result call observation\" ... ok\n",
            "test \"result local value flow\" ... ok\n",
            "12 test(s) passed\n",
        )
    );
    assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_flow_fixture_is_formatter_stable() {
    let expected = fs::read_to_string("../tests/option-result-flow.slo").expect("read fixture");
    let output = run_glagol(["--format", "../tests/option-result-flow.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "formatter rejected option/result flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "formatter output drifted");
    assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_flow_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/option-result-flow.slo",
    ]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    let surface_stderr = String::from_utf8_lossy(&surface.stderr);
    assert!(
        surface.status.success(),
        "surface lowering rejected option/result flow fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn option_score(value: (option i32)) -> i32")
            && surface_stdout.contains("fn option_wide_score(value: (option i64)) -> i32")
            && surface_stdout.contains("fn option_string_score(value: (option string)) -> i32")
            && surface_stdout.contains("is_some")
            && surface_stdout.contains("is_none")
            && surface_stdout.contains("is_ok")
            && surface_stdout.contains("is_err"),
        "surface lowering output lost option/result flow shape\nstdout:\n{}",
        surface_stdout
    );
    assert!(
        surface_stderr.is_empty(),
        "surface lowering wrote stderr:\n{}",
        surface_stderr
    );

    let checked = run_glagol([
        "--inspect-lowering=checked",
        "../examples/option-result-flow.slo",
    ]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    let checked_stderr = String::from_utf8_lossy(&checked.stderr);
    assert!(
        checked.status.success(),
        "checked lowering rejected option/result flow fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("is_some : bool")
            && checked_stdout.contains("is_none : bool")
            && checked_stdout.contains("is_ok : bool")
            && checked_stdout.contains("is_err : bool")
            && checked_stdout.contains("call maybe_wide_value : (option i64)")
            && checked_stdout.contains("call maybe_string_value : (option string)")
            && checked_stdout.contains("var value : (option i64)")
            && checked_stdout.contains("var value : (option string)")
            && checked_stdout.contains("local let value : unit"),
        "checked lowering output lost typed option/result flow shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn option_result_payload_fixture_emits_llvm_unwrap_shape() {
    let output = run_glagol(["../examples/option-result-payload.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("declare void @__glagol_unwrap_some_trap()")
            && stdout.contains("declare void @__glagol_unwrap_ok_trap()")
            && stdout.contains("declare void @__glagol_unwrap_err_trap()")
            && stdout.contains("define i32 @option_direct_payload()")
            && stdout.contains("define i32 @option_param_payload({ i1, i32 } %value)")
            && stdout.contains("define i32 @option_local_payload()")
            && stdout.contains("define i32 @option_call_payload()")
            && stdout.contains("define i32 @option_guarded_payload({ i1, i32 } %value)")
            && stdout.contains("define ptr @option_string_direct_payload()")
            && stdout.contains("define ptr @option_string_param_payload({ i1, ptr } %value)")
            && stdout.contains("define ptr @option_string_local_payload()")
            && stdout.contains("define ptr @option_string_call_payload()")
            && stdout.contains("define ptr @option_string_guarded_payload({ i1, ptr } %value)")
            && stdout.contains("define i32 @result_ok_direct_payload()")
            && stdout.contains("define i32 @result_err_direct_payload()")
            && stdout.contains("define i32 @result_ok_param_payload({ i1, i32 } %value)")
            && stdout.contains("define i32 @result_err_param_payload({ i1, i32 } %value)")
            && stdout.contains("define i32 @result_ok_local_payload()")
            && stdout.contains("define i32 @result_err_call_payload()")
            && stdout.contains("call void @__glagol_unwrap_some_trap()")
            && stdout.contains("call void @__glagol_unwrap_ok_trap()")
            && stdout.contains("call void @__glagol_unwrap_err_trap()")
            && stdout.contains("unwrap.trap")
            && stdout.contains("if.then")
            && stdout.contains(" phi i32 ")
            && stdout.contains(" phi ptr ")
            && stdout.contains("extractvalue { i1, i32 } %"),
        "LLVM output did not contain expected option/result payload-access shape\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_payload_fixture_runs_top_level_tests() {
    let output = run_glagol(["--run-tests", "../examples/option-result-payload.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "test runner rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(
        stdout,
        concat!(
            "test \"unwrap some direct constructor\" ... ok\n",
            "test \"unwrap some local\" ... ok\n",
            "test \"unwrap some call\" ... ok\n",
            "test \"unwrap some parameter\" ... ok\n",
            "test \"guarded unwrap some present\" ... ok\n",
            "test \"guarded unwrap some absent\" ... ok\n",
            "test \"unwrap some i64 direct constructor\" ... ok\n",
            "test \"unwrap some i64 local\" ... ok\n",
            "test \"unwrap some i64 call\" ... ok\n",
            "test \"unwrap some i64 parameter\" ... ok\n",
            "test \"guarded unwrap some i64 present\" ... ok\n",
            "test \"guarded unwrap some i64 absent\" ... ok\n",
            "test \"unwrap some f64 direct constructor\" ... ok\n",
            "test \"unwrap some f64 local\" ... ok\n",
            "test \"unwrap some f64 call\" ... ok\n",
            "test \"unwrap some f64 parameter\" ... ok\n",
            "test \"guarded unwrap some f64 present\" ... ok\n",
            "test \"guarded unwrap some f64 absent\" ... ok\n",
            "test \"unwrap some bool direct constructor\" ... ok\n",
            "test \"unwrap some bool local\" ... ok\n",
            "test \"unwrap some bool call\" ... ok\n",
            "test \"unwrap some bool parameter\" ... ok\n",
            "test \"guarded unwrap some bool present\" ... ok\n",
            "test \"guarded unwrap some bool absent\" ... ok\n",
            "test \"unwrap some string direct constructor\" ... ok\n",
            "test \"unwrap some string local\" ... ok\n",
            "test \"unwrap some string call\" ... ok\n",
            "test \"unwrap some string parameter\" ... ok\n",
            "test \"guarded unwrap some string present\" ... ok\n",
            "test \"guarded unwrap some string absent\" ... ok\n",
            "test \"unwrap ok direct constructor\" ... ok\n",
            "test \"unwrap err direct constructor\" ... ok\n",
            "test \"unwrap ok parameter\" ... ok\n",
            "test \"unwrap err parameter\" ... ok\n",
            "test \"unwrap ok local\" ... ok\n",
            "test \"unwrap err call\" ... ok\n",
            "36 test(s) passed\n",
        )
    );
    assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_payload_fixture_is_formatter_stable() {
    let expected = fs::read_to_string("../tests/option-result-payload.slo").expect("read fixture");
    let output = run_glagol(["--format", "../tests/option-result-payload.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "formatter rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "formatter output drifted");
    assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_payload_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/option-result-payload.slo",
    ]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    let surface_stderr = String::from_utf8_lossy(&surface.stderr);
    assert!(
        surface.status.success(),
        "surface lowering rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn option_direct_payload() -> i32")
            && surface_stdout.contains("fn option_param_payload(value: (option i32)) -> i32")
            && surface_stdout.contains("fn option_guarded_payload(value: (option i32)) -> i32")
            && surface_stdout.contains("fn option_wide_direct_payload() -> i64")
            && surface_stdout.contains("fn option_wide_param_payload(value: (option i64)) -> i64")
            && surface_stdout
                .contains("fn option_wide_guarded_payload(value: (option i64)) -> i64")
            && surface_stdout.contains("fn option_string_direct_payload() -> string")
            && surface_stdout
                .contains("fn option_string_param_payload(value: (option string)) -> string")
            && surface_stdout
                .contains("fn option_string_guarded_payload(value: (option string)) -> string")
            && surface_stdout.contains("fn result_ok_direct_payload() -> i32")
            && surface_stdout
                .contains("fn result_err_param_payload(value: (result i32 i32)) -> i32")
            && surface_stdout.contains("is_some")
            && surface_stdout.contains("unwrap_some")
            && surface_stdout.contains("unwrap_ok")
            && surface_stdout.contains("unwrap_err"),
        "surface lowering output lost option/result payload shape\nstdout:\n{}",
        surface_stdout
    );
    assert!(
        surface_stderr.is_empty(),
        "surface lowering wrote stderr:\n{}",
        surface_stderr
    );

    let checked = run_glagol([
        "--inspect-lowering=checked",
        "../examples/option-result-payload.slo",
    ]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    let checked_stderr = String::from_utf8_lossy(&checked.stderr);
    assert!(
        checked.status.success(),
        "checked lowering rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("unwrap_some : i32")
            && checked_stdout.contains("unwrap_some : i64")
            && checked_stdout.contains("unwrap_some : string")
            && checked_stdout.contains("unwrap_ok : i32")
            && checked_stdout.contains("unwrap_err : i32")
            && checked_stdout.contains("some : (option i32)")
            && checked_stdout.contains("some : (option i64)")
            && checked_stdout.contains("some : (option string)")
            && checked_stdout.contains("err : (result i32 i32)")
            && checked_stdout.contains("is_some : bool"),
        "checked lowering output lost typed option/result payload shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn option_result_payload_llvm_traps_before_extracting_payload() {
    let output = run_glagol(["../examples/option-result-payload.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected option/result payload fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );

    assert_unwrap_traps_before_payload_extract(
        &function_body(
            &stdout,
            "define i32 @option_param_payload({ i1, i32 } %value)",
        ),
        false,
    );
    assert_unwrap_traps_before_payload_extract(
        &function_body(
            &stdout,
            "define ptr @option_string_param_payload({ i1, ptr } %value)",
        ),
        false,
    );
    assert_unwrap_traps_before_payload_extract(
        &function_body(
            &stdout,
            "define i32 @result_ok_param_payload({ i1, i32 } %value)",
        ),
        false,
    );
    assert_unwrap_traps_before_payload_extract(
        &function_body(
            &stdout,
            "define i32 @result_err_param_payload({ i1, i32 } %value)",
        ),
        true,
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_match_fixture_emits_llvm_branch_shape() {
    let output = run_glagol(["../examples/option-result-match.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected option/result match fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define i32 @option_value_or({ i1, i32 } %value, i32 %fallback)")
            && stdout.contains("define i32 @option_bump_or_zero({ i1, i32 } %value)")
            && stdout
                .contains("define ptr @option_string_value_or({ i1, ptr } %value, ptr %fallback)")
            && stdout.contains(
                "define ptr @option_string_selected_or({ i1, ptr } %value, ptr %fallback)"
            )
            && stdout.contains("define i32 @result_value_or_code({ i1, i32 } %value)")
            && stdout.contains("define i32 @result_score({ i1, i32 } %value)")
            && stdout.contains("match.some")
            && stdout.contains("match.none")
            && stdout.contains("match.ok")
            && stdout.contains("match.err")
            && stdout.contains("match.end")
            && stdout.contains("br i1")
            && stdout.contains(" phi i32 ")
            && stdout.contains(" phi ptr ")
            && stdout.contains("extractvalue { i1, i32 } %"),
        "LLVM output did not contain expected match branch shape\nstdout:\n{}",
        stdout
    );
    assert!(
        !function_body(
            &stdout,
            "define i32 @option_value_or({ i1, i32 } %value, i32 %fallback)",
        )
        .contains("__glagol_unwrap"),
        "match lowering must not call unwrap traps\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_match_fixture_runs_top_level_tests() {
    let output = run_glagol(["--run-tests", "../examples/option-result-match.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "test runner rejected option/result match fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(
        stdout,
        concat!(
            "test \"option match some payload\" ... ok\n",
            "test \"option match none fallback\" ... ok\n",
            "test \"option match multi expression arm\" ... ok\n",
            "test \"option i64 match some payload\" ... ok\n",
            "test \"option i64 match none fallback\" ... ok\n",
            "test \"option i64 match multi expression arm\" ... ok\n",
            "test \"option f64 match some payload\" ... ok\n",
            "test \"option f64 match none fallback\" ... ok\n",
            "test \"option f64 match multi expression arm\" ... ok\n",
            "test \"option bool match some payload\" ... ok\n",
            "test \"option bool match none fallback\" ... ok\n",
            "test \"option bool match multi expression arm\" ... ok\n",
            "test \"option string match some payload\" ... ok\n",
            "test \"option string match none fallback\" ... ok\n",
            "test \"option string match multi expression arm\" ... ok\n",
            "test \"result match ok payload\" ... ok\n",
            "test \"result match err payload\" ... ok\n",
            "test \"result match computed arm\" ... ok\n",
            "18 test(s) passed\n",
        )
    );
    assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_match_fixture_is_formatter_stable() {
    let expected = fs::read_to_string("../tests/option-result-match.slo").expect("read fixture");
    let output = run_glagol(["--format", "../tests/option-result-match.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "formatter rejected option/result match fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "formatter output drifted");
    assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
}

#[test]
fn option_result_match_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/option-result-match.slo",
    ]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    let surface_stderr = String::from_utf8_lossy(&surface.stderr);
    assert!(
        surface.status.success(),
        "surface lowering rejected option/result match fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("match")
            && surface_stdout.contains("subject")
            && surface_stdout.contains("arm some payload")
            && surface_stdout.contains("arm none")
            && surface_stdout.contains("arm ok payload")
            && surface_stdout.contains("arm err code"),
        "surface lowering output lost match shape\nstdout:\n{}",
        surface_stdout
    );
    assert!(
        surface_stderr.is_empty(),
        "surface lowering wrote stderr:\n{}",
        surface_stderr
    );

    let checked = run_glagol([
        "--inspect-lowering=checked",
        "../examples/option-result-match.slo",
    ]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    let checked_stderr = String::from_utf8_lossy(&checked.stderr);
    assert!(
        checked.status.success(),
        "checked lowering rejected option/result match fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("match : i32")
            && checked_stdout.contains("var payload : i32")
            && checked_stdout.contains("var payload : i64")
            && checked_stdout.contains("var fallback : i32")
            && checked_stdout.contains("var fallback : i64")
            && checked_stdout.contains("var code : i32"),
        "checked lowering output lost typed match shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

fn function_body<'a>(ir: &'a str, signature: &str) -> &'a str {
    let start = ir
        .find(signature)
        .unwrap_or_else(|| panic!("missing function signature `{}`\n{}", signature, ir));
    let rest = &ir[start..];
    let end = rest
        .find("\n}\n")
        .unwrap_or_else(|| panic!("missing function end for `{}`", signature));
    &rest[..end]
}

fn assert_unwrap_traps_before_payload_extract(body: &str, expects_inverted_tag: bool) {
    let tag_extract = body
        .find(", 0")
        .unwrap_or_else(|| panic!("missing tag extract in function body:\n{}", body));
    let branch = body
        .find("br i1")
        .unwrap_or_else(|| panic!("missing unwrap branch in function body:\n{}", body));
    let trap_call = body
        .find("call void @__glagol_unwrap")
        .unwrap_or_else(|| panic!("missing unwrap trap call in function body:\n{}", body));
    let payload_extract = body
        .rfind(", 1")
        .unwrap_or_else(|| panic!("missing payload extract in function body:\n{}", body));

    assert!(
        tag_extract < branch && branch < trap_call && trap_call < payload_extract,
        "unwrap payload extraction was not guarded by a prior tag trap path\n{}",
        body
    );
    assert!(
        body.contains("unreachable"),
        "unwrap trap block must terminate with unreachable\n{}",
        body
    );

    if expects_inverted_tag {
        assert!(
            body.contains("icmp eq i1"),
            "unwrap_err must branch on the inverted result tag\n{}",
            body
        );
    }
}

fn run_glagol<const N: usize>(args: [&str; N]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run glagol")
}
