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

const UNSAFE_HEADS: &[&str] = &[
    "alloc",
    "dealloc",
    "load",
    "store",
    "ptr_add",
    "unchecked_index",
    "reinterpret",
    "ffi_call",
];

#[test]
fn unsafe_fixture_is_formatter_stable() {
    let fixture = Path::new("../tests/unsafe.slo");
    let expected = fs::read_to_string(fixture).expect("read unsafe formatter fixture");

    let output = run_glagol(["--format", fixture.to_str().expect("fixture path is UTF-8")]);

    assert_success_stdout(output, &expected, "unsafe formatter output");
}

#[test]
fn unsafe_fixture_prints_expected_surface_ast() {
    let expected =
        fs::read_to_string("../tests/unsafe.surface.lower").expect("read surface fixture");
    let output = run_glagol(["--inspect-lowering=surface", "../tests/unsafe.slo"]);

    assert_success_stdout(output, &expected, "unsafe surface lowering output");
}

#[test]
fn unsafe_fixture_prints_expected_checked_ast() {
    let expected =
        fs::read_to_string("../tests/unsafe.checked.lower").expect("read checked fixture");
    let output = run_glagol(["--inspect-lowering=checked", "../tests/unsafe.slo"]);

    assert_success_stdout(output, &expected, "unsafe checked lowering output");
}

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

    assert!(
        output.status.success(),
        "compiler rejected unsafe fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define i32 @add_one_in_unsafe(i32 %value)")
            && stdout.contains("%0 = add i32 %value, 1")
            && stdout.contains("ret i32 %0")
            && !stdout.contains("%one.addr = alloca i32")
            && !stdout.contains("load i32, ptr %one.addr")
            && stdout.contains("define i32 @main()"),
        "compiler output for unsafe fixture lost safe block shape\nstdout:\n{}",
        stdout
    );
    assert!(
        !stdout.contains("unsafe block returns final value")
            && !stdout.contains("unsafe block can return bool"),
        "compiler output should not emit top-level test names as LLVM metadata\nstdout:\n{}",
        stdout
    );
    assert!(
        stderr.is_empty(),
        "compiler wrote stderr for unsafe fixture:\n{}",
        stderr
    );
}

#[test]
fn unsafe_fixture_runs_top_level_tests() {
    let output = run_glagol(["--run-tests", "../examples/unsafe.slo"]);

    assert_success_stdout(
        output,
        concat!(
            "test \"unsafe block returns final value\" ... ok\n",
            "test \"unsafe block can return bool\" ... ok\n",
            "2 test(s) passed\n",
        ),
        "unsafe test runner output",
    );
}

#[test]
fn unsafe_operation_heads_require_marker_before_function_lookup() {
    for head in UNSAFE_HEADS {
        let source = format!("(module main)\n\n(fn main () -> i32\n  ({} 1))\n", head);
        let output = run_glagol_source(&format!("unsafe-required-{}", head), &source);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "compiler accepted unsafe operation `{}` outside unsafe",
            head
        );
        assert!(
            stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnknownFunction"),
            "unsafe operation `{}` did not produce UnsafeRequired before call lookup\nstderr:\n{}",
            head,
            stderr
        );
    }

    let output = run_glagol_source(
        "reserved-unsafe-head",
        "(module main)\n\n(fn alloc () -> i32\n  1)\n\n(fn main () -> i32\n  (alloc))\n",
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success(),
        "compiler allowed user-defined unsafe head call outside unsafe"
    );
    assert!(
        stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnknownFunction"),
        "reserved unsafe head did not produce UnsafeRequired before call lookup\nstderr:\n{}",
        stderr
    );
}

#[test]
fn unsafe_operation_heads_are_reserved_from_user_bindings() {
    for head in UNSAFE_HEADS {
        let local_function = format!("(module main)\n\n(fn {} () -> i32\n  1)\n", head);
        let output = run_glagol_source(&format!("reserved-function-{}", head), &local_function);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success() && stderr.contains("(code DuplicateFunction)"),
            "compiler allowed unsafe head `{}` as a function\nstderr:\n{}",
            head,
            stderr
        );

        let parameter = format!(
            "(module main)\n\n(fn main (({} i32)) -> i32\n  {})\n",
            head, head
        );
        let output = run_glagol_source(&format!("reserved-param-{}", head), &parameter);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success() && stderr.contains("(code ParameterShadowsCallable)"),
            "compiler allowed unsafe head `{}` as a parameter\nstderr:\n{}",
            head,
            stderr
        );

        let local = format!(
            "(module main)\n\n(fn main () -> i32\n  (let {} i32 1)\n  {})\n",
            head, head
        );
        let output = run_glagol_source(&format!("reserved-local-{}", head), &local);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success() && stderr.contains("(code LocalShadowsCallable)"),
            "compiler allowed unsafe head `{}` as a local\nstderr:\n{}",
            head,
            stderr
        );
    }
}

#[test]
fn unsafe_operation_heads_remain_unsupported_inside_marker() {
    for head in UNSAFE_HEADS {
        let source = format!(
            "(module main)\n\n(fn main () -> i32\n  (unsafe\n    ({} 1)))\n",
            head
        );
        let output = run_glagol_source(&format!("unsupported-unsafe-{}", head), &source);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "compiler accepted raw unsafe operation `{}` inside unsafe",
            head
        );
        assert!(
            stderr.contains("(code UnsupportedUnsafeOperation)")
                && !stderr.contains("UnknownFunction"),
            "unsafe operation `{}` did not produce UnsupportedUnsafeOperation inside unsafe\nstderr:\n{}",
            head,
            stderr
        );
    }
}

#[test]
fn scoped_unsafe_locals_get_hygienic_llvm_storage_names() {
    let source = r#"
(module main)

(fn scoped () -> i32
  (unsafe
    (var one i32 1)
    (print_i32 one))
  (unsafe
    (var one i32 2)
    (print_i32 one))
  (var one i32 3)
  one)

(fn main () -> i32
  (scoped))
"#;
    let output = run_glagol_source("unsafe-scoped-locals", source);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected scoped unsafe locals\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.matches("%one.addr = alloca i32").count() == 1
            && stdout.matches("%one.addr.1 = alloca i32").count() == 1
            && stdout.matches("%one.addr.2 = alloca i32").count() == 1,
        "LLVM output did not assign hygienic local storage names\nstdout:\n{}",
        stdout
    );
}

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

fn run_glagol_source(name: &str, source: &str) -> std::process::Output {
    let path =
        std::env::temp_dir().join(format!("glagol-unsafe-{}-{}.slo", name, std::process::id()));
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    let output = Command::new(env!("CARGO_BIN_EXE_glagol"))
        .arg(&path)
        .output()
        .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", path.display(), err));
    let _ = fs::remove_file(path);
    output
}

fn assert_success_stdout(output: std::process::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);
}
