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 i64_fixture_lowers_and_runs_tests() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/i64-numeric-primitive.slo");

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile i64 fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("declare void @print_i64(i64)")
            && stdout.contains("define i64 @base()")
            && stdout.contains("add i64")
            && stdout.contains("mul i64")
            && stdout.contains("icmp sgt i64")
            && stdout.contains("icmp slt i64")
            && stdout.contains("icmp eq i64")
            && stdout.contains("call void @print_i64(i64"),
        "i64 LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let division_fixture = write_fixture(
        "division-lowering",
        "(module main)\n\n(fn quotient ((value i64)) -> i64\n  (/ value 3i64))\n\n(fn main () -> i32\n  (if (>= (quotient 4294967289i64) 1431655763i64) 0 1))\n",
    );
    let division_llvm = run_glagol([division_fixture.as_os_str()]);
    assert_success("compile i64 division lowering", &division_llvm);
    let division_stdout = String::from_utf8_lossy(&division_llvm.stdout);
    assert!(
        division_stdout.contains("sdiv i64") && division_stdout.contains("icmp sge i64"),
        "i64 division/order LLVM shape drifted\nstdout:\n{}",
        division_stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run i64 fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"i64 arithmetic returns exact fixture value\" ... ok\n",
            "test \"i64 comparison works in predicates\" ... ok\n",
            "test \"i64 division and ordering\" ... ok\n",
            "3 test(s) passed\n",
        ),
        "i64 test runner stdout drifted"
    );
}

#[test]
fn i64_formatter_and_lowering_are_visible() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/i64-numeric-primitive.slo");

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format i64 fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(fn base () -> i64")
            && formatted_stdout.contains("2147483648i64")
            && formatted_stdout.contains("(std.io.print_i64 (local_total))"),
        "i64 formatter output omitted expected forms\nstdout:\n{}",
        formatted_stdout
    );

    let surface = run_glagol([
        OsStr::new("--inspect-lowering=surface"),
        fixture.as_os_str(),
    ]);
    assert_success("inspect i64 surface lowering", &surface);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    assert_eq!(
        surface_stdout,
        fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/i64-numeric-primitive.surface.lower")
        )
        .expect("read i64 surface snapshot"),
        "i64 surface lowering snapshot drifted"
    );

    let checked = run_glagol([
        OsStr::new("--inspect-lowering=checked"),
        fixture.as_os_str(),
    ]);
    assert_success("inspect i64 checked lowering", &checked);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    assert_eq!(
        checked_stdout,
        fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/i64-numeric-primitive.checked.lower")
        )
        .expect("read i64 checked snapshot"),
        "i64 checked lowering snapshot drifted"
    );
}

#[test]
fn mixed_i32_i64_f64_operands_are_rejected_clearly() {
    for (name, expression, found) in [
        ("mixed-i32-i64", "(= 1 1i64)", "i32 and i64"),
        ("mixed-i64-f64", "(= 1i64 1.0)", "i64 and f64"),
    ] {
        let fixture = write_fixture(
            name,
            &format!(
                "(module main)\n\n(fn main () -> i32\n  (if {} 0 1))\n",
                expression
            ),
        );
        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 mixed numeric operands\nstdout:\n{}\nstderr:\n{}",
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected compile wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("numeric operands must have the same primitive type")
                && stderr.contains("mixed i32/i64/u32/u64/f64")
                && stderr.contains(found),
            "mixed numeric diagnostic drifted for {}\nstderr:\n{}",
            name,
            stderr
        );
    }
}

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

    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/i64-numeric-primitive.slo");
    let compile = run_glagol([fixture.as_os_str()]);
    assert_success("compile i64 runtime smoke", &compile);
    let run = compile_and_run_with_runtime(&clang, "i64-runtime-smoke", &compile.stdout);
    assert_success("run i64 runtime smoke", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "4294967289\n",
        "i64 runtime print stdout drifted"
    );
}

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-i64-alpha-{}-{}-{}.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-i64-alpha-{}-{}",
        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 i64 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);
}
