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_time_lowers_to_private_runtime_helpers() {
    let fixture = write_fixture(
        "lowering",
        r#"
(module main)

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

    assert!(
        stdout.contains("declare i32 @__glagol_time_monotonic_ms()")
            && stdout.contains("declare void @__glagol_time_sleep_ms(i32)")
            && stdout.contains("call void @__glagol_time_sleep_ms(i32 0)")
            && stdout.contains("call i32 @__glagol_time_monotonic_ms()")
            && !stdout.contains("@std.time."),
        "standard time LLVM shape drifted\nstdout:\n{}",
        stdout
    );
}

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

(fn sleep_zero_then_one () -> i32
  (std.time.sleep_ms 0)
  1)

(test "monotonic is non-negative"
  (>= (std.time.monotonic_ms) 0))

(test "sleep zero returns"
  (= (sleep_zero_then_one) 1))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run standard time tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"monotonic is non-negative\" ... ok\n",
            "test \"sleep zero returns\" ... ok\n",
            "2 test(s) passed\n",
        ),
        "standard time test runner stdout drifted"
    );
}

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

(fn bad_sleep () -> i32
  (std.time.sleep_ms -1)
  0)

(test "negative sleep traps"
  (= (bad_sleep) 0))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert_eq!(
        output.status.code(),
        Some(1),
        "negative sleep trap exit code drifted\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.is_empty(),
        "negative sleep trap wrote stdout:\n{}",
        stdout
    );
    assert!(
        stderr.contains("TestRuntimeTrap")
            && stderr.contains("slovo runtime error: sleep_ms negative duration"),
        "negative sleep trap diagnostic drifted\nstderr:\n{}",
        stderr
    );
}

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

(fn main () -> i32
  (std.time.monotonic_ms 1))
"#,
            "ArityMismatch",
        ),
        (
            "sleep-arity",
            r#"
(module main)

(fn main () -> i32
  (std.time.sleep_ms)
  0)
"#,
            "ArityMismatch",
        ),
        (
            "sleep-type",
            r#"
(module main)

(fn main () -> i32
  (std.time.sleep_ms true)
  0)
"#,
            "TypeMismatch",
        ),
        (
            "unknown-time",
            r#"
(module main)

(fn main () -> i32
  (std.time.now))
"#,
            "UnsupportedStandardLibraryCall",
        ),
        (
            "promoted-shadow",
            r#"
(module main)

(fn std.time.monotonic_ms () -> i32
  0)

(fn main () -> i32
  (std.time.monotonic_ms))
"#,
            "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_and_negative_sleep_trap_when_clang_is_available() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping standard time runtime smoke: set GLAGOL_CLANG or install clang");
        return;
    };

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

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

    let trap = write_fixture(
        "runtime-negative-sleep",
        r#"
(module main)

(fn main () -> i32
  (std.time.sleep_ms -1)
  0)
"#,
    );
    let compile = run_glagol([trap.as_os_str()]);
    assert_success("compile standard time negative sleep trap", &compile);
    let run = compile_and_run_with_runtime(&clang, "standard-time-negative-sleep", &compile.stdout);
    assert_eq!(
        run.status.code(),
        Some(1),
        "negative sleep runtime exit code drifted\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&run.stdout),
        String::from_utf8_lossy(&run.stderr)
    );
    assert_eq!(
        String::from_utf8_lossy(&run.stderr),
        "slovo runtime error: sleep_ms negative duration\n",
        "negative sleep runtime stderr drifted"
    );
    assert!(
        run.stdout.is_empty(),
        "negative sleep runtime wrote stdout:\n{}",
        String::from_utf8_lossy(&run.stdout)
    );
}

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