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

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

#[test]
fn standard_random_lowers_to_private_runtime_helper() {
    let fixture = write_fixture(
        "lowering",
        r#"
(module main)

(fn main () -> i32
  (std.random.i32))
"#,
    );
    let output = run_glagol([fixture.as_os_str()]);
    assert_success("compile standard random lowering", &output);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        stdout.contains("declare i32 @__glagol_random_i32()")
            && stdout.contains("call i32 @__glagol_random_i32()")
            && !stdout.contains("@std.random"),
        "standard random LLVM shape drifted\nstdout:\n{}",
        stdout
    );
}

#[test]
fn test_runner_executes_random_i32_as_non_negative() {
    let fixture = write_fixture(
        "test-runner",
        r#"
(module main)

(test "random i32 is non-negative"
  (if (< (std.random.i32) 0)
    false
    true))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run standard random tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"random i32 is non-negative\" ... ok\n",
            "1 test(s) passed\n",
        ),
        "standard random test runner stdout drifted"
    );
}

#[test]
fn standard_random_diagnostics_cover_promoted_and_deferred_names() {
    let cases = [
        (
            "random-arity",
            r#"
(module main)

(fn main () -> i32
  (std.random.i32 1))
"#,
            "ArityMismatch",
        ),
        (
            "random-bool-context",
            r#"
(module main)

(fn main () -> i32
  (if (std.random.i32) 1 0))
"#,
            "IfConditionNotBool",
        ),
        (
            "unknown-random",
            r#"
(module main)

(fn main () -> i32
  (std.random.range 0 10))
"#,
            "UnsupportedStandardLibraryCall",
        ),
        (
            "promoted-shadow",
            r#"
(module main)

(fn std.random.i32 () -> i32
  0)

(fn main () -> i32
  (std.random.i32))
"#,
            "DuplicateFunction",
        ),
    ];

    for (name, source, diagnostic) in cases {
        let fixture = write_fixture(name, source);
        let output = run_glagol([fixture.as_os_str()]);
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected compile wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains(diagnostic),
            "diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
            diagnostic,
            name,
            stderr
        );
    }
}

#[test]
fn hosted_runtime_smoke_when_clang_is_available() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping standard random runtime smoke: set GLAGOL_CLANG or install clang");
        return;
    };

    let smoke = write_fixture(
        "runtime-smoke",
        r#"
(module main)

(fn main () -> i32
  (if (< (std.random.i32) 0) 1 0))
"#,
    );
    let compile = run_glagol([smoke.as_os_str()]);
    assert_success("compile standard random runtime smoke", &compile);
    let run = compile_and_run_with_runtime(&clang, "standard-random-smoke", &compile.stdout);
    assert_success("run standard random runtime smoke", &run);
}

fn run_glagol<I, S>(args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run glagol")
}

fn write_fixture(name: &str, source: &str) -> PathBuf {
    let mut path = env::temp_dir();
    path.push(format!(
        "glagol-standard-random-{}-{}-{}.slo",
        name,
        std::process::id(),
        NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
    ));
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn assert_success(context: &str, output: &Output) {
    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
    );
}

fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output {
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    let temp_dir = env::temp_dir().join(format!(
        "glagol-standard-random-{}-{}",
        std::process::id(),
        NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
    ));
    fs::create_dir_all(&temp_dir)
        .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));

    let ir_path = temp_dir.join(format!("{}.ll", name));
    let exe_path = temp_dir.join(name);
    fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));

    let runtime = manifest.join("../runtime/runtime.c");
    let mut clang_command = Command::new(clang);
    clang_command
        .arg(&runtime)
        .arg(&ir_path)
        .arg("-o")
        .arg(&exe_path)
        .current_dir(manifest);
    configure_clang_runtime_env(&mut clang_command, clang);
    let clang_output = clang_command
        .output()
        .unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
    assert_success("clang standard random runtime smoke", &clang_output);

    Command::new(&exe_path)
        .output()
        .unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err))
}

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);
}
