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

const SUPPORTED_COMPILE_FIXTURES: &[&str] = &[
    "../examples/add.slo",
    "../examples/array-direct-scalars-value-flow.slo",
    "../examples/array-direct-scalars.slo",
    "../examples/array-enum.slo",
    "../examples/array-struct-elements.slo",
    "../examples/array-struct-fields.slo",
    "../examples/array-string-value-flow.slo",
    "../examples/array-string.slo",
    "../examples/array-value-flow.slo",
    "../examples/array.slo",
    "../examples/enum-payload-structs.slo",
    "../examples/if.slo",
    "../examples/local-variables.slo",
    "../examples/option-result-flow.slo",
    "../examples/option-result-match.slo",
    "../examples/option-result-payload.slo",
    "../examples/option-result.slo",
    "../examples/owned-string-concat.slo",
    "../examples/f64-to-i32-result.slo",
    "../examples/f64-to-i64-result.slo",
    "../examples/f64-to-string.slo",
    "../examples/print-bool.slo",
    "../examples/standard-runtime.slo",
    "../examples/string-parse-bool-result.slo",
    "../examples/string-parse-i64-result.slo",
    "../examples/string-parse-f64-result.slo",
    "../examples/string-print.slo",
    "../examples/string-value-flow.slo",
    "../examples/struct-value-flow.slo",
    "../examples/struct.slo",
    "../examples/unsafe.slo",
    "../examples/while.slo",
];

#[test]
#[ignore = "requires clang or GLAGOL_CLANG; optional promotion smoke for emitted LLVM IR"]
fn emitted_supported_fixture_ir_compiles_with_clang_when_available() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping LLVM smoke test: set GLAGOL_CLANG or install clang");
        return;
    };

    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime = manifest.join("../runtime/runtime.c");
    let temp_dir = env::temp_dir().join(format!("glagol-llvm-smoke-{}", std::process::id()));
    fs::create_dir_all(&temp_dir)
        .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));

    for fixture in SUPPORTED_COMPILE_FIXTURES {
        let ir = emit_fixture_ir(manifest, fixture);
        let stem = fixture_stem(fixture);
        let ir_path = temp_dir.join(format!("{}.ll", stem));
        let exe_path = temp_dir.join(stem);
        fs::write(&ir_path, ir)
            .unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));

        let mut command = Command::new(&clang);
        command
            .arg(&runtime)
            .arg(&ir_path)
            .arg("-o")
            .arg(&exe_path)
            .current_dir(manifest);
        configure_clang_runtime_env(&mut command, &clang);
        let output = command
            .output()
            .unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            output.status.success(),
            "clang rejected emitted LLVM for `{}`\nstdout:\n{}\nstderr:\n{}",
            fixture,
            stdout,
            stderr
        );
    }
}

fn emit_fixture_ir(manifest: &Path, fixture: &str) -> String {
    let output = Command::new(env!("CARGO_BIN_EXE_glagol"))
        .arg(fixture)
        .current_dir(manifest)
        .output()
        .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture, err));
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "glagol rejected `{}`\nstdout:\n{}\nstderr:\n{}",
        fixture,
        stdout,
        stderr
    );
    assert!(
        stderr.is_empty(),
        "glagol wrote stderr for `{}`: {}",
        fixture,
        stderr
    );
    stdout.into_owned()
}

fn find_clang() -> Option<PathBuf> {
    if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
        return Some(PathBuf::from(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(program: &str) -> Option<PathBuf> {
    let path = env::var_os("PATH")?;
    env::split_paths(&path)
        .map(|dir| dir.join(program))
        .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 mut paths = vec![lib64, lib];

    if let Some(existing) = env::var_os("LD_LIBRARY_PATH") {
        paths.extend(env::split_paths(&existing));
    }

    let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
    command.env("LD_LIBRARY_PATH", joined);
}

fn fixture_stem(fixture: &str) -> String {
    Path::new(fixture)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .expect("fixture has utf-8 file stem")
        .replace('-', "_")
}
