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

#[test]
fn direct_scalar_array_fixture_emits_direct_scalar_llvm_shapes() {
    let output = run_glagol(["../examples/array-direct-scalars.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected direct-scalar array fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define i32 @i32_second()")
            && stdout.contains("define i64 @i64_local_pick()")
            && stdout.contains("define double @f64_third()")
            && stdout.contains("define i1 @bool_local_pick()")
            && stdout.contains("insertvalue [3 x i32]")
            && stdout.contains("insertvalue [3 x double]")
            && stdout.contains("alloca [3 x i64]")
            && stdout.contains("alloca [3 x i1]"),
        "LLVM output did not contain the widened direct-scalar array shapes\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("i32 direct scalar array index")
            && !stdout.contains("bool local direct scalar array index"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn direct_scalar_array_fixture_runs_tests_and_formats_stably() {
    let run = run_glagol(["--run-tests", "../examples/array-direct-scalars.slo"]);
    assert_success_stdout(
        run,
        concat!(
            "test \"i32 direct scalar array index\" ... ok\n",
            "test \"i64 local direct scalar array index\" ... ok\n",
            "test \"f64 direct scalar array index\" ... ok\n",
            "test \"bool local direct scalar array index\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "direct-scalar array test runner output",
    );

    let expected = fs::read_to_string("../tests/array-direct-scalars.slo").expect("read fixture");
    let format = run_glagol(["--format", "../tests/array-direct-scalars.slo"]);
    assert_success_stdout(format, &expected, "direct-scalar array formatter output");
}

#[test]
fn direct_scalar_array_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/array-direct-scalars.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 direct-scalar array fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("array i64")
            && surface_stdout.contains("array f64")
            && surface_stdout.contains("array bool")
            && surface_stdout.contains("local let values: (array i64 3)")
            && surface_stdout.contains("local let flags: (array bool 3)"),
        "surface lowering output lost direct-scalar array 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/array-direct-scalars.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 direct-scalar array fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("fn i32_second() -> i32")
            && checked_stdout.contains("fn i64_local_pick() -> i64")
            && checked_stdout.contains("fn f64_third() -> f64")
            && checked_stdout.contains("fn bool_local_pick() -> bool")
            && checked_stdout.contains("index : i32")
            && checked_stdout.contains("index : i64")
            && checked_stdout.contains("index : f64")
            && checked_stdout.contains("index : bool"),
        "checked lowering output lost typed direct-scalar array shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn direct_scalar_array_value_flow_fixture_emits_llvm_value_flow_and_bounds_shape() {
    let output = run_glagol(["../examples/array-direct-scalars-value-flow.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected direct-scalar array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define [3 x i32] @make_i32_values(i32 %base)")
            && stdout.contains("define [3 x i64] @make_i64_values(i64 %base)")
            && stdout.contains("define [3 x double] @make_f64_values(double %base)")
            && stdout.contains("define [3 x i1] @make_flags(i1 %first)")
            && stdout.contains("define i32 @i32_at([3 x i32] %values, i32 %i)")
            && stdout.contains("define i64 @i64_at([3 x i64] %values, i32 %i)")
            && stdout.contains("define double @f64_at([3 x double] %values, i32 %i)")
            && stdout.contains("define [3 x i1] @echo_flags([3 x i1] %values)")
            && stdout.contains("call void @__glagol_array_bounds_trap()")
            && stdout.contains("array.index.trap")
            && stdout.contains("getelementptr inbounds [3 x i64]")
            && stdout.contains("getelementptr inbounds [3 x double]"),
        "LLVM output did not contain expected direct-scalar array value-flow shape\nstdout:\n{}",
        stdout
    );
    let dynamic_index_function =
        llvm_function(&stdout, "define i64 @i64_at([3 x i64] %values, i32 %i)");
    assert_contains_in_order(
        dynamic_index_function,
        &[
            "icmp sge i32 %i, 0",
            "icmp slt i32 %i, 3",
            "array.index.trap",
            "call void @__glagol_array_bounds_trap()",
            "array.index.ok",
            "getelementptr inbounds [3 x i64]",
            "load i64, ptr %",
        ],
        "LLVM output did not emit bounds-check shape before direct-scalar array indexing",
    );
    assert!(
        !stdout.contains("i64 array local call value flow")
            && !stdout.contains("bool array dynamic index"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn direct_scalar_array_value_flow_fixture_runs_tests_and_formats_stably() {
    let run = run_glagol([
        "--run-tests",
        "../examples/array-direct-scalars-value-flow.slo",
    ]);
    assert_success_stdout(
        run,
        concat!(
            "test \"i32 array parameter value flow\" ... ok\n",
            "test \"i64 array local call value flow\" ... ok\n",
            "test \"f64 array parameter local copy\" ... ok\n",
            "test \"bool array dynamic index\" ... ok\n",
            "test \"bool array return call value flow\" ... ok\n",
            "5 test(s) passed\n",
        ),
        "direct-scalar array value-flow test runner output",
    );

    let expected =
        fs::read_to_string("../tests/array-direct-scalars-value-flow.slo").expect("read fixture");
    let format = run_glagol(["--format", "../tests/array-direct-scalars-value-flow.slo"]);
    assert_success_stdout(
        format,
        &expected,
        "direct-scalar array value-flow formatter output",
    );
}

#[test]
fn direct_scalar_array_value_flow_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/array-direct-scalars-value-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 direct-scalar array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn make_i32_values(base: i32) -> (array i32 3)")
            && surface_stdout.contains("fn make_i64_values(base: i64) -> (array i64 3)")
            && surface_stdout.contains("fn make_f64_values(base: f64) -> (array f64 3)")
            && surface_stdout.contains("fn make_flags(first: bool) -> (array bool 3)")
            && surface_stdout.contains("fn echo_flags(values: (array bool 3)) -> (array bool 3)")
            && surface_stdout.contains("local let copy: (array f64 3)"),
        "surface lowering output lost direct-scalar array value-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/array-direct-scalars-value-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 direct-scalar array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("fn make_i32_values(base: i32) -> (array i32 3)")
            && checked_stdout.contains("fn make_i64_values(base: i64) -> (array i64 3)")
            && checked_stdout.contains("fn f64_at(values: (array f64 3), i: i32) -> f64")
            && checked_stdout.contains("fn echo_flags(values: (array bool 3)) -> (array bool 3)")
            && checked_stdout.contains("call make_i64_values : (array i64 3)")
            && checked_stdout.contains("index : i64")
            && checked_stdout.contains("index : f64")
            && checked_stdout.contains("index : bool"),
        "checked lowering output lost typed direct-scalar array value-flow shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

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

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "{} drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn llvm_function<'a>(source: &'a str, signature: &str) -> &'a str {
    let start = source
        .find(signature)
        .unwrap_or_else(|| panic!("LLVM output omitted `{}`\nstdout:\n{}", signature, source));
    let tail = &source[start..];
    let end = tail.find("\n}\n").unwrap_or_else(|| {
        panic!(
            "LLVM function `{}` was not closed\nstdout:\n{}",
            signature, source
        )
    });
    &tail[..end + "\n}\n".len()]
}

fn assert_contains_in_order(source: &str, needles: &[&str], message: &str) {
    let mut cursor = 0;
    for needle in needles {
        let relative = source[cursor..]
            .find(needle)
            .unwrap_or_else(|| panic!("{}\nmissing `{}`\nsource:\n{}", message, needle, source));
        cursor += relative + needle.len();
    }
}
