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

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

    assert!(
        output.status.success(),
        "compiler rejected string-array fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define ptr @immediate_second()")
            && stdout.contains("define ptr @local_pick()")
            && stdout.contains("insertvalue [3 x ptr]")
            && stdout.contains("alloca [3 x ptr]"),
        "LLVM output did not contain the string-array shapes\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("string immediate array index")
            && !stdout.contains("string local array index"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn string_array_fixture_runs_tests_and_formats_stably() {
    let run = run_glagol(["--run-tests", "../examples/array-string.slo"]);
    assert_success_stdout(
        run,
        concat!(
            "test \"string immediate array index\" ... ok\n",
            "test \"string local array index\" ... ok\n",
            "2 test(s) passed\n",
        ),
        "string-array test runner output",
    );

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

#[test]
fn string_array_fixture_prints_lowered_shape() {
    let surface = run_glagol(["--inspect-lowering=surface", "../examples/array-string.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 string-array fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("array string")
            && surface_stdout.contains("local let words: (array string 3)"),
        "surface lowering output lost string-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-string.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 string-array fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("fn immediate_second() -> string")
            && checked_stdout.contains("fn local_pick() -> string")
            && checked_stdout.contains("index : string"),
        "checked lowering output lost typed string-array shape\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_stderr
    );
}

#[test]
fn string_array_value_flow_fixture_emits_llvm_value_flow_and_bounds_shape() {
    let output = run_glagol(["../examples/array-string-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 string-array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define [3 x ptr] @make_words(ptr %head)")
            && stdout.contains("define ptr @at([3 x ptr] %values, i32 %i)")
            && stdout.contains("define [3 x ptr] @echo([3 x ptr] %values)")
            && stdout.contains("call void @__glagol_array_bounds_trap()")
            && stdout.contains("array.index.trap")
            && stdout.contains("getelementptr inbounds [3 x ptr]"),
        "LLVM output did not contain expected string-array value-flow shape\nstdout:\n{}",
        stdout
    );
    let dynamic_index_function =
        llvm_function(&stdout, "define ptr @at([3 x ptr] %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 ptr]",
            "load ptr, ptr %",
        ],
        "LLVM output did not emit bounds-check shape before string-array indexing",
    );
    assert!(
        !stdout.contains("string array local call value flow")
            && !stdout.contains("string array dynamic index"),
        "compiler emitted test metadata into LLVM output\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

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

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

#[test]
fn string_array_value_flow_fixture_prints_lowered_shape() {
    let surface = run_glagol([
        "--inspect-lowering=surface",
        "../examples/array-string-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 string-array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("fn make_words(head: string) -> (array string 3)")
            && surface_stdout.contains("fn at(values: (array string 3), i: i32) -> string")
            && surface_stdout.contains("fn echo(values: (array string 3)) -> (array string 3)")
            && surface_stdout.contains("local let copy: (array string 3)"),
        "surface lowering output lost string-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-string-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 string-array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("fn make_words(head: string) -> (array string 3)")
            && checked_stdout.contains("fn at(values: (array string 3), i: i32) -> string")
            && checked_stdout.contains("fn echo(values: (array string 3)) -> (array string 3)")
            && checked_stdout.contains("call make_words : (array string 3)")
            && checked_stdout.contains("index : string"),
        "checked lowering output lost typed string-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();
    }
}
