use std::{
    env, fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);

#[test]
fn run_manifest_records_success_report_stdout_and_program_args() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping run manifest success report: set GLAGOL_CLANG or install clang");
        return;
    };

    let source = write_fixture(
        "success",
        r#"
(module main)

(fn main () -> i32
  (print_string "beta22-out")
  0)
"#,
        "slo",
    );
    let manifest_path = temp_path("success-manifest", "manifest.slo");

    let mut command = Command::new(compiler_path());
    command
        .arg("run")
        .arg(&source)
        .arg("--manifest")
        .arg(&manifest_path)
        .arg("--")
        .arg("alpha")
        .arg("two words")
        .arg("--literal")
        .env("GLAGOL_CLANG", &clang);
    configure_clang_runtime_env(&mut command, &clang);

    let output = command.output().expect("run glagol success manifest");
    assert_success_stdout("run manifest success", &output, "beta22-out\n");

    let manifest = read_manifest(&manifest_path);
    assert!(
        manifest.contains("  (mode run)\n")
            && manifest.contains("  (success true)\n")
            && manifest.contains("  (run-report\n")
            && manifest.contains("    (exit-status 0)\n")
            && manifest.contains("    (stdout \"beta22-out\\n\")\n")
            && manifest.contains("    (stderr \"\")\n")
            && manifest.contains("      (arg \"alpha\")\n")
            && manifest.contains("      (arg \"two words\")\n")
            && manifest.contains("      (arg \"--literal\")\n"),
        "successful run manifest mismatch:\n{}",
        manifest
    );
}

#[test]
fn run_manifest_records_nonzero_report_and_preserves_program_stderr() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping run manifest nonzero report: set GLAGOL_CLANG or install clang");
        return;
    };

    let source = write_fixture(
        "nonzero",
        r#"
(module main)

(import_c beta22_stderr () -> i32)

(fn emit_stderr () -> i32
  (unsafe
    (beta22_stderr)))

(fn main () -> i32
  (emit_stderr)
  7)
"#,
        "slo",
    );
    let c_source = write_fixture(
        "nonzero-stderr",
        r#"
#include <stdio.h>

int beta22_stderr(void) {
    fputs("beta22-err\n", stderr);
    return 0;
}
"#,
        "c",
    );
    let manifest_path = temp_path("nonzero-manifest", "manifest.slo");

    let mut command = Command::new(compiler_path());
    command
        .arg("run")
        .arg(&source)
        .arg("--link-c")
        .arg(&c_source)
        .arg("--manifest")
        .arg(&manifest_path)
        .env("GLAGOL_CLANG", &clang);
    configure_clang_runtime_env(&mut command, &clang);

    let output = command.output().expect("run glagol nonzero manifest");
    assert_exit_code("run manifest nonzero", &output, 7);
    assert_eq!(output.stdout, b"", "nonzero run stdout drifted");
    assert_eq!(output.stderr, b"beta22-err\n", "nonzero run stderr drifted");

    let manifest = read_manifest(&manifest_path);
    assert!(
        manifest.contains("  (mode run)\n")
            && manifest.contains("  (success false)\n")
            && manifest.contains("  (run-report\n")
            && manifest.contains("    (exit-status 7)\n")
            && manifest.contains("    (stdout \"\")\n")
            && manifest.contains("    (stderr \"beta22-err\\n\")\n")
            && manifest.contains("    (args)\n"),
        "nonzero run manifest mismatch:\n{}",
        manifest
    );
}

#[test]
fn run_manifest_source_failure_does_not_record_fake_run_report() {
    let source = write_fixture(
        "source-failure",
        r#"
(module main)

(fn main () -> i32
  true)
"#,
        "slo",
    );
    let manifest_path = temp_path("source-failure-manifest", "manifest.slo");

    let output = run_glagol([
        "run".as_ref(),
        source.as_os_str(),
        "--manifest".as_ref(),
        manifest_path.as_os_str(),
    ]);
    assert_exit_code("run manifest source failure", &output, 1);

    let manifest = read_manifest(&manifest_path);
    assert!(
        manifest.contains("  (mode run)\n")
            && manifest.contains("  (success false)\n")
            && manifest.contains("    (kind diagnostics)\n")
            && manifest.contains("TypeMismatch")
            && !manifest.contains("  (run-report\n"),
        "source failure manifest included fake run report:\n{}",
        manifest
    );
}

fn run_glagol<const N: usize>(args: [&std::ffi::OsStr; N]) -> Output {
    Command::new(compiler_path())
        .args(args)
        .output()
        .expect("run glagol")
}

fn compiler_path() -> &'static str {
    env!("CARGO_BIN_EXE_glagol")
}

fn write_fixture(name: &str, source: &str, extension: &str) -> PathBuf {
    let path = temp_path(name, extension);
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn temp_path(name: &str, extension: &str) -> PathBuf {
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    let mut path = env::temp_dir();
    path.push(format!(
        "glagol-run-manifest-beta22-{}-{}-{}.{}",
        std::process::id(),
        id,
        name,
        extension
    ));
    path
}

fn read_manifest(path: &Path) -> String {
    fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
}

fn assert_success_stdout(context: &str, output: &Output, expected: &str) {
    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        expected,
        "{} stdout mismatch",
        context
    );
    assert!(
        output.stderr.is_empty(),
        "{} wrote stderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stderr)
    );
}

fn assert_exit_code(context: &str, output: &Output, expected: i32) {
    assert_eq!(
        output.status.code(),
        Some(expected),
        "{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn find_clang() -> Option<PathBuf> {
    if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
        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);
}
