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

#[test]
fn c_import_emits_declare_and_direct_call_inside_unsafe() {
    let output = run_glagol(["../examples/ffi/exp-6-c-add/main.slo"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "compiler rejected C FFI fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("declare i32 @c_add(i32, i32)")
            && stdout.contains("call i32 @c_add(i32 40, i32 2)")
            && stdout.contains("define i32 @main()"),
        "LLVM output lost C import declaration/call shape\nstdout:\n{}",
        stdout
    );
}

#[test]
fn c_import_call_requires_unsafe_before_backend_behavior() {
    let output = run_glagol_source(
        "c-ffi-unsafe-required",
        "(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) -> i32)\n\n(fn main () -> i32\n  (c_add 1 2))\n",
        &[],
    );
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "compiler accepted safe C import call"
    );
    assert!(
        stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnsupportedBackendFeature"),
        "C import call outside unsafe did not produce UnsafeRequired first\nstderr:\n{}",
        stderr
    );
}

#[test]
fn c_import_rejects_unsupported_signature_types() {
    for (name, source) in [
        (
            "bool-param",
            "(module main)\n\n(import_c c_bool ((flag bool)) -> i32)\n\n(fn main () -> i32\n  0)\n",
        ),
        (
            "string-return",
            "(module main)\n\n(import_c c_string () -> string)\n\n(fn main () -> i32\n  0)\n",
        ),
        (
            "array-param",
            "(module main)\n\n(import_c c_array ((xs (array i32 2))) -> i32)\n\n(fn main () -> i32\n  0)\n",
        ),
    ] {
        let output = run_glagol_source(&format!("c-ffi-{}", name), source, &[]);
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            !output.status.success() && stderr.contains("(code UnsupportedCImportType)"),
            "unsupported C import type `{}` did not produce UnsupportedCImportType\nstderr:\n{}",
            name,
            stderr
        );
    }
}

#[test]
fn c_import_rejects_malformed_duplicate_and_reserved_names() {
    let malformed = run_glagol_source(
        "c-ffi-malformed",
        "(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) i32)\n\n(fn main () -> i32\n  0)\n",
        &[],
    );
    assert_diagnostic(&malformed, "MalformedCImport");

    let invalid_symbol = run_glagol_source(
        "c-ffi-invalid-symbol",
        "(module main)\n\n(import_c c-add () -> i32)\n\n(fn main () -> i32\n  0)\n",
        &[],
    );
    assert_diagnostic(&invalid_symbol, "MalformedCImport");

    let duplicate_param = run_glagol_source(
        "c-ffi-duplicate-param",
        "(module main)\n\n(import_c c_add ((value i32) (value i32)) -> i32)\n\n(fn main () -> i32\n  0)\n",
        &[],
    );
    assert_diagnostic(&duplicate_param, "DuplicateName");

    let duplicate = run_glagol_source(
        "c-ffi-duplicate",
        "(module main)\n\n(fn c_add () -> i32\n  1)\n\n(import_c c_add () -> i32)\n\n(fn main () -> i32\n  (c_add))\n",
        &[],
    );
    assert_diagnostic(&duplicate, "DuplicateTopLevelName");

    let reserved = run_glagol_source(
        "c-ffi-reserved",
        "(module main)\n\n(import_c ffi_call () -> i32)\n\n(fn main () -> i32\n  0)\n",
        &[],
    );
    assert_diagnostic(&reserved, "ReservedName");
}

#[test]
fn raw_unsafe_ffi_call_remains_unsupported_inside_unsafe() {
    let output = run_glagol_source(
        "c-ffi-raw-ffi-call",
        "(module main)\n\n(fn main () -> i32\n  (unsafe\n    (ffi_call 1)))\n",
        &[],
    );
    assert_diagnostic(&output, "UnsupportedUnsafeOperation");
}

#[test]
fn formatter_and_lowering_show_c_imports() {
    let fixture = "../examples/ffi/exp-6-c-add/main.slo";
    let formatted = run_glagol(["--format", fixture]);
    let stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted.status.success()
            && stdout.contains("(import_c c_add ((lhs i32) (rhs i32)) -> i32)"),
        "formatter did not preserve import_c\nstdout:\n{}\nstderr:\n{}",
        stdout,
        String::from_utf8_lossy(&formatted.stderr)
    );

    let surface = run_glagol(["--inspect-lowering=surface", fixture]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    assert!(
        surface.status.success()
            && surface_stdout.contains("  import_c c_add(lhs: i32, rhs: i32) -> i32"),
        "surface lowering did not show C import\nstdout:\n{}",
        surface_stdout
    );

    let checked = run_glagol(["--inspect-lowering=checked", fixture]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    assert!(
        checked.status.success()
            && checked_stdout.contains("  import_c c_add(lhs: i32, rhs: i32) -> i32"),
        "checked lowering did not show C import\nstdout:\n{}",
        checked_stdout
    );
}

#[test]
fn test_runner_reports_c_imports_as_unsupported_execution() {
    let output = run_glagol(["--run-tests", "../examples/ffi/exp-6-c-add/main.slo"]);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success() && stderr.contains("(code UnsupportedTestExpression)"),
        "test runner did not report explicit C import execution diagnostic\nstderr:\n{}",
        stderr
    );
}

#[test]
fn test_runner_filter_skips_unselected_c_import_execution() {
    let source = "(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) -> i32)\n\n(test \"pure selected\"\n  true)\n\n(test \"ffi skipped\"\n  (= (unsafe\n    (c_add 1 2)) 3))\n";
    let path = temp_path("c-ffi-filter-skips-test-runner", "input.slo");
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));

    let skipped = run_glagol_os([
        "--run-tests".as_ref(),
        "--filter".as_ref(),
        "pure".as_ref(),
        path.as_os_str(),
    ]);
    let stdout = String::from_utf8_lossy(&skipped.stdout);
    let stderr = String::from_utf8_lossy(&skipped.stderr);
    assert!(
        skipped.status.success()
            && stdout.contains("test \"ffi skipped\" ... skipped")
            && stdout.contains("selected 1")
            && stdout.contains("skipped 1")
            && !stderr.contains("UnsupportedTestExpression"),
        "filtered run evaluated skipped C import test\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );

    let selected = run_glagol_os([
        "--run-tests".as_ref(),
        "--filter".as_ref(),
        "ffi".as_ref(),
        path.as_os_str(),
    ]);
    let selected_stderr = String::from_utf8_lossy(&selected.stderr);
    assert!(
        !selected.status.success() && selected_stderr.contains("(code UnsupportedTestExpression)"),
        "selected C import test did not keep unsupported execution diagnostic\nstderr:\n{}",
        selected_stderr
    );
}

#[test]
fn unit_return_c_import_is_supported_and_release_gated() {
    let source = "(module main)\n\n(import_c c_log ((value i32)) -> unit)\n\n(fn main () -> i32\n  (unsafe\n    (c_log 7))\n  0)\n";

    let llvm = run_glagol_source("c-ffi-unit-return-llvm", source, &[]);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        llvm.status.success()
            && stdout.contains("declare void @c_log(i32)")
            && stdout.contains("call void @c_log(i32 7)")
            && stdout.contains("define i32 @main()"),
        "unit C import did not lower to void declaration/call\nstdout:\n{}\nstderr:\n{}",
        stdout,
        String::from_utf8_lossy(&llvm.stderr)
    );

    let formatted = run_glagol_source("c-ffi-unit-return-fmt", source, &["--format"]);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted.status.success()
            && formatted_stdout.contains("(import_c c_log ((value i32)) -> unit)")
            && formatted_stdout.contains("(c_log 7)"),
        "formatter did not preserve unit C import\nstdout:\n{}\nstderr:\n{}",
        formatted_stdout,
        String::from_utf8_lossy(&formatted.stderr)
    );

    let source_path = temp_path("c-ffi-unit-return-manifest", "input.slo");
    let manifest = temp_path("c-ffi-unit-return-manifest", "manifest.slo");
    fs::write(&source_path, source)
        .unwrap_or_else(|err| panic!("write `{}`: {}", source_path.display(), err));
    let manifest_output = run_glagol_os([
        "check".as_ref(),
        source_path.as_os_str(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
    ]);
    assert_success("unit C FFI check manifest", &manifest_output);
    let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
    assert!(
        manifest_text.contains("(foreign_import")
            && manifest_text.contains("(source_module \"main\")")
            && manifest_text.contains("(name \"c_log\")")
            && manifest_text.contains("(return \"unit\")"),
        "unit C import manifest did not record source module and return metadata\n{}",
        manifest_text
    );

    let test_source = "(module main)\n\n(import_c c_log ((value i32)) -> unit)\n\n(fn main () -> i32\n  0)\n\n(test \"unit C import test runner diagnostic\"\n  (var i i32 0)\n  (while (< i 1)\n    (unsafe\n      (c_log i))\n    (set i (+ i 1)))\n  (= i 1))\n";
    let test_path = temp_path("c-ffi-unit-return-test-runner", "input.slo");
    fs::write(&test_path, test_source)
        .unwrap_or_else(|err| panic!("write `{}`: {}", test_path.display(), err));
    let test_output = run_glagol_os(["--run-tests".as_ref(), test_path.as_os_str()]);
    let test_stderr = String::from_utf8_lossy(&test_output.stderr);
    assert!(
        !test_output.status.success() && test_stderr.contains("(code UnsupportedTestExpression)"),
        "test runner did not reject reached unit C import execution\nstderr:\n{}",
        test_stderr
    );
}

#[test]
fn single_file_manifest_records_foreign_import_metadata() {
    let manifest = temp_path("c-ffi-single-file-manifest", "manifest.slo");
    let output = run_glagol_os([
        "check".as_ref(),
        "../examples/ffi/exp-6-c-add/main.slo".as_ref(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
    ]);
    assert_success("single-file C FFI check manifest", &output);

    let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
    assert!(
        manifest_text.contains("(foreign_import")
            && manifest_text.contains("(name \"c_add\")")
            && manifest_text.contains("(source_module \"main\")")
            && manifest_text.contains("(symbol \"c_add\")")
            && manifest_text.contains("(abi \"experimental-fixture-c\")"),
        "single-file manifest did not record C import metadata\n{}",
        manifest_text
    );
}

#[test]
fn project_mode_emits_foreign_import_and_manifest_metadata() {
    let fixture = "../examples/ffi/exp-6-c-add";
    let manifest = temp_path("c-ffi-project-manifest", "manifest.slo");
    let binary = temp_path("c-ffi-project-manifest", "bin");
    let output = run_glagol_os([
        "build".as_ref(),
        fixture.as_ref(),
        "-o".as_ref(),
        binary.as_os_str(),
        "--link-c".as_ref(),
        "../examples/ffi/exp-6-c-add/c_add.c".as_ref(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
    ]);
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        if stderr.contains("ToolchainUnavailable") {
            eprintln!("skipping project manifest assertion: clang is unavailable");
            return;
        }
        panic!(
            "project build failed\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            stderr
        );
    }
    let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
    assert!(
        manifest_text.contains("(foreign_import")
            && manifest_text.contains("(name \"c_add\")")
            && manifest_text.contains("(source_module \"main\")")
            && manifest_text.contains("(symbol \"c_add\")")
            && manifest_text.contains("(input \"../examples/ffi/exp-6-c-add/c_add.c\")"),
        "manifest did not record C import/link input\n{}",
        manifest_text
    );
}

#[test]
fn hosted_build_with_link_c_runs_when_clang_is_available() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping C FFI hosted build: set GLAGOL_CLANG or install clang");
        return;
    };

    let binary = temp_path("c-ffi-hosted", "bin");
    let mut build_command = Command::new(env!("CARGO_BIN_EXE_glagol"));
    build_command
        .args([
            "build".as_ref(),
            "../examples/ffi/exp-6-c-add".as_ref(),
            "-o".as_ref(),
            binary.as_os_str(),
            "--link-c".as_ref(),
            "../examples/ffi/exp-6-c-add/c_add.c".as_ref(),
        ])
        .env("GLAGOL_CLANG", &clang);
    configure_clang_runtime_env(&mut build_command, &clang);
    let build = build_command
        .output()
        .expect("run glagol hosted C FFI build");
    assert_success("hosted C FFI build", &build);

    let run = Command::new(&binary)
        .output()
        .expect("run hosted C FFI binary");
    assert!(
        run.status.code() == Some(42),
        "hosted C FFI binary returned unexpected status\nstdout:\n{}\nstderr:\n{:?}",
        String::from_utf8_lossy(&run.stdout),
        String::from_utf8_lossy(&run.stderr)
    );
}

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_os<const N: usize>(args: [&std::ffi::OsStr; 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, extra_args: &[&str]) -> std::process::Output {
    let path = temp_path(name, "input.slo");
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
    command.arg(&path);
    command.args(extra_args);
    command
        .output()
        .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", path.display(), err))
}

fn assert_diagnostic(output: &std::process::Output, code: &str) {
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success() && stderr.contains(&format!("(code {})", code)),
        "expected diagnostic `{}`\nstdout:\n{}\nstderr:\n{}",
        code,
        String::from_utf8_lossy(&output.stdout),
        stderr
    );
}

fn assert_success(context: &str, output: &std::process::Output) {
    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn temp_path(name: &str, file: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("glagol-{}-{}", name, std::process::id()));
    fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
    dir.join(file)
}

fn find_clang() -> Option<PathBuf> {
    if let Ok(path) = std::env::var("GLAGOL_CLANG") {
        let path = PathBuf::from(path);
        if path.is_file() {
            return Some(path);
        }
    }
    let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang");
    if hermetic_clang.is_file() {
        return Some(hermetic_clang);
    }
    find_on_path("clang")
}

fn find_on_path(name: &str) -> Option<PathBuf> {
    let path = env::var_os("PATH")?;
    env::split_paths(&path)
        .map(|dir| dir.join(name))
        .find(|candidate| candidate.is_file())
}

fn configure_clang_runtime_env(command: &mut Command, clang: &Path) {
    if !clang.starts_with("/tmp/glagol-clang-root") {
        return;
    }

    let root = Path::new("/tmp/glagol-clang-root");
    let lib64 = root.join("usr/lib64");
    let lib = root.join("usr/lib");
    let existing = env::var_os("LD_LIBRARY_PATH").unwrap_or_default();
    let mut paths = vec![lib64, lib];
    paths.extend(env::split_paths(&existing));
    let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
    command.env("LD_LIBRARY_PATH", joined);
}
