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

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

    assert!(
        output.status.success(),
        "compiler rejected array fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define i32 @immediate_second()")
            && stdout.contains("insertvalue [3 x i32]")
            && stdout.contains("extractvalue [3 x i32]")
            && stdout.contains("%values.addr = alloca [3 x i32]")
            && stdout.contains("getelementptr inbounds [3 x i32]")
            && stdout.contains("load i32"),
        "LLVM output did not contain expected array index shape\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("immediate array index") && !stdout.contains("array local index"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn immediate_literal_array_index_evaluates_all_element_expressions_in_order() {
    let fixture = write_fixture(
        "immediate-literal-array-index-evaluation",
        r#"(module main)

(fn first_value () -> i32
  11)

(fn second_value () -> i32
  22)

(fn third_value () -> i32
  33)

(fn selected_immediate () -> i32
  (index (array i32 (first_value) (second_value) (third_value)) 1))

(test "immediate literal index evaluates array"
  (= (selected_immediate) 22))

(fn main () -> i32
  (selected_immediate))
"#,
    );
    let fixture_arg = fixture.to_string_lossy().into_owned();
    let output = run_glagol([fixture_arg.as_str()]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected immediate literal array index regression fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );

    let first_call = single_occurrence_index(&stdout, "call i32 @first_value()");
    let second_call = single_occurrence_index(&stdout, "call i32 @second_value()");
    let third_call = single_occurrence_index(&stdout, "call i32 @third_value()");
    assert!(
        first_call < second_call && second_call < third_call,
        "LLVM output did not emit array element calls in source order\nstdout:\n{}",
        stdout
    );
    assert!(
        stdout.contains("extractvalue [3 x i32]"),
        "LLVM output did not select from the constructed aggregate\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);

    let run = run_glagol(["--run-tests", fixture_arg.as_str()]);
    let run_stdout = String::from_utf8_lossy(&run.stdout);
    let run_stderr = String::from_utf8_lossy(&run.stderr);
    assert!(
        run.status.success(),
        "test runner rejected immediate literal array index regression fixture\nstdout:\n{}\nstderr:\n{}",
        run_stdout,
        run_stderr
    );
    assert_eq!(
        run_stdout,
        concat!(
            "test \"immediate literal index evaluates array\" ... ok\n",
            "1 test(s) passed\n",
        ),
        "test runner output drifted"
    );
    assert!(
        run_stderr.is_empty(),
        "test runner wrote stderr:\n{}",
        run_stderr
    );
}

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

    assert!(
        output.status.success(),
        "test runner rejected array fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(
        stdout,
        concat!(
            "test \"immediate array index\" ... ok\n",
            "test \"array local index\" ... ok\n",
            "2 test(s) passed\n",
        ),
        "test runner output drifted"
    );
    assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
}

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

    assert!(
        output.status.success(),
        "formatter rejected array 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 array_fixture_prints_lowered_shape() {
    let surface = run_glagol(["--inspect-lowering=surface", "../examples/array.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 array fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("array i32")
            && surface_stdout.contains("index")
            && surface_stdout.contains("local let values: (array i32 3)"),
        "surface lowering output lost 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.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 array fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("array : (array i32 3)")
            && checked_stdout.contains("index : i32")
            && checked_stdout.contains("var values : (array i32 3)"),
        "checked lowering output lost typed array shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn array_value_flow_fixture_emits_llvm_value_flow_and_bounds_shape() {
    let output = run_glagol(["../examples/array-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 array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define [3 x i32] @make_values(i32 %base)")
            && stdout.contains("define i32 @first([3 x i32] %values)")
            && stdout.contains("define i32 @at([3 x i32] %values, i32 %i)")
            && stdout.contains("define [3 x i32] @echo([3 x i32] %values)")
            && stdout.contains("define i32 @parameter_local_copy([3 x i32] %values, i32 %i)")
            && stdout.contains("call [3 x i32] @make_values(i32 20)")
            && stdout.contains("call i32 @at([3 x i32]")
            && stdout.contains("load [3 x i32], ptr %values.addr"),
        "LLVM output did not contain expected array value-flow shape\nstdout:\n{}",
        stdout
    );
    assert!(
        stdout.contains("declare void @__glagol_array_bounds_trap()"),
        "LLVM output did not declare the array bounds trap\nstdout:\n{}",
        stdout
    );
    let dynamic_index_function =
        llvm_function(&stdout, "define i32 @at([3 x i32] %values, i32 %i)");
    assert_contains_in_order(
        dynamic_index_function,
        &[
            "icmp sge i32 %i, 0",
            "icmp slt i32 %i, 3",
            "br i1 %",
            "array.index.trap",
            "call void @__glagol_array_bounds_trap()",
            "unreachable",
            "array.index.ok",
            "getelementptr inbounds [3 x i32]",
            "i32 %i",
            "load i32, ptr %",
        ],
        "LLVM output did not emit bounds branch/trap before the dynamic access",
    );
    assert!(
        !stdout.contains("array dynamic index") && !stdout.contains("array parameter local copy"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn array_value_flow_fixture_runs_top_level_tests() {
    let output = run_glagol(["--run-tests", "../examples/array-value-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 array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(
        stdout,
        concat!(
            "test \"array parameter value flow\" ... ok\n",
            "test \"array dynamic index\" ... ok\n",
            "test \"array local call value flow\" ... ok\n",
            "test \"array parameter local copy\" ... ok\n",
            "test \"array return call value flow\" ... ok\n",
            "5 test(s) passed\n",
        ),
        "test runner output drifted"
    );
    assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
}

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

    assert!(
        output.status.success(),
        "formatter rejected array value-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 array_value_flow_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/array-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 array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn make_values(base: i32) -> (array i32 3)")
            && surface_stdout.contains("fn at(values: (array i32 3), i: i32) -> i32")
            && surface_stdout.contains("fn echo(values: (array i32 3)) -> (array i32 3)")
            && surface_stdout.contains("local let values: (array i32 3)")
            && surface_stdout.contains("index"),
        "surface lowering output lost 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-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 array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("fn make_values(base: i32) -> (array i32 3)")
            && checked_stdout.contains("fn at(values: (array i32 3), i: i32) -> i32")
            && checked_stdout.contains("call make_values : (array i32 3)")
            && checked_stdout.contains("local let values : unit")
            && checked_stdout.contains("index : i32"),
        "checked lowering output lost typed 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]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run glagol")
}

fn write_fixture(name: &str, source: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    path.push(format!(
        "glagol-array-index-{}-{}.slo",
        name,
        std::process::id()
    ));
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn single_occurrence_index(source: &str, needle: &str) -> usize {
    let mut matches = source.match_indices(needle);
    let (index, _) = matches
        .next()
        .unwrap_or_else(|| panic!("LLVM output omitted `{}`\nstdout:\n{}", needle, source));
    assert!(
        matches.next().is_none(),
        "LLVM output contained `{}` more than once\nstdout:\n{}",
        needle,
        source
    );
    index
}

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();
    }
}
