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_io_host_env_fixture_lowers_to_runtime_helpers() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/standard-io-host-env.slo");
    let compile = run_glagol([fixture.as_os_str()]);
    let stdout = String::from_utf8_lossy(&compile.stdout);
    let stderr = String::from_utf8_lossy(&compile.stderr);

    assert!(
        compile.status.success(),
        "compiler rejected exp-3 fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("declare void @__glagol_io_eprint(ptr)")
            && stdout.contains("declare void @__glagol_process_init(i32, ptr)")
            && stdout.contains("declare i32 @__glagol_process_argc()")
            && stdout.contains("declare ptr @__glagol_process_arg(i32)")
            && stdout.contains("declare ptr @__glagol_env_get(ptr)")
            && stdout.contains("declare ptr @__glagol_fs_read_text(ptr)")
            && stdout.contains("declare i32 @__glagol_fs_write_text(ptr, ptr)")
            && stdout.contains("define i32 @main(i32 %__glagol_argc, ptr %__glagol_argv)")
            && stdout.contains(
                "call void @__glagol_process_init(i32 %__glagol_argc, ptr %__glagol_argv)"
            )
            && stdout.contains("call void @__glagol_io_eprint(ptr @")
            && stdout.contains("call i32 @__glagol_process_argc()")
            && stdout.contains("call ptr @__glagol_process_arg(i32 0)")
            && stdout.contains("call ptr @__glagol_env_get(ptr @")
            && stdout.contains("call ptr @__glagol_fs_read_text(ptr @")
            && stdout.contains("call i32 @__glagol_fs_write_text(ptr @")
            && !stdout.contains("@std."),
        "LLVM output did not contain expected exp-3 runtime shape\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn standard_io_host_env_test_runner_interprets_host_calls() {
    let root = temp_root("test-runner");
    let input = root.join("input.txt");
    let output = root.join("output.txt");
    fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
    fs::write(&input, "hello").unwrap_or_else(|err| panic!("write `{}`: {}", input.display(), err));

    let source = format!(
        r#"
(module main)

(test "env missing is empty"
  (= (std.env.get "GLAGOL_EXP_3_MISSING") ""))

(test "fs roundtrip"
  (= (std.fs.write_text "{}" (std.fs.read_text "{}")) 0))
"#,
        slovo_path(&output),
        slovo_path(&input)
    );
    let fixture = write_fixture("host-test-runner", &source);
    let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);

    assert_success_stdout(
        run,
        concat!(
            "test \"env missing is empty\" ... ok\n",
            "test \"fs roundtrip\" ... ok\n",
            "2 test(s) passed\n",
        ),
        "exp-3 test runner output",
    );
    assert_eq!(
        fs::read_to_string(&output).expect("read roundtrip output"),
        "hello"
    );
}

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

    let root = temp_root("runtime");
    let input = root.join("input.txt");
    let output = root.join("output.txt");
    fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
    fs::write(&input, "runtime text")
        .unwrap_or_else(|err| panic!("write `{}`: {}", input.display(), err));

    let source = format!(
        r#"
(module main)

(fn main () -> i32
  (std.io.eprint "err-line\n")
  (std.io.print_i32 (std.process.argc))
  (std.io.print_string (std.process.arg 1))
  (std.io.print_string (std.env.get "GLAGOL_EXP_3_PRESENT"))
  (let text string (std.fs.read_text "{}"))
  (std.io.print_string text)
  (std.fs.write_text "{}" (std.string.concat text "-written")))
"#,
        slovo_path(&input),
        slovo_path(&output)
    );
    let fixture = write_fixture("host-runtime", &source);
    let compile = run_glagol([fixture.as_os_str()]);
    assert_success("compile exp-3 runtime smoke", &compile);

    let run = compile_and_run_with_runtime(&clang, "host-runtime", &compile.stdout, |command| {
        command
            .arg("argument-value")
            .env("GLAGOL_EXP_3_PRESENT", "env-value");
    });
    assert_eq!(
        run.status.code(),
        Some(0),
        "exp-3 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.stdout),
        "2\nargument-value\nenv-value\nruntime text\n",
        "exp-3 runtime stdout drifted"
    );
    assert_eq!(
        String::from_utf8_lossy(&run.stderr),
        "err-line\n",
        "exp-3 runtime stderr drifted"
    );
    assert_eq!(
        fs::read_to_string(&output).expect("read exp-3 write output"),
        "runtime text-written"
    );
}

#[test]
fn standard_io_host_env_runtime_traps_when_clang_is_available() {
    let Some(clang) = find_clang() else {
        eprintln!("skipping exp-3 trap smoke: set GLAGOL_CLANG or install clang");
        return;
    };

    let cases = [
        (
            "process-arg-oob",
            r#"
(module main)

(fn main () -> i32
  (std.string.len (std.process.arg 99)))
"#,
            "slovo runtime error: process argument index out of bounds\n",
        ),
        (
            "file-read-failed",
            r#"
(module main)

(fn main () -> i32
  (std.string.len (std.fs.read_text "/tmp/glagol-exp-3-missing-file-for-trap.txt")))
"#,
            "slovo runtime error: file read failed\n",
        ),
    ];

    for (name, source, expected_stderr) in cases {
        let fixture = write_fixture(name, source);
        let compile = run_glagol([fixture.as_os_str()]);
        assert_success("compile exp-3 trap smoke", &compile);
        let run = compile_and_run_with_runtime(&clang, name, &compile.stdout, |_| {});

        assert_eq!(
            run.status.code(),
            Some(1),
            "exp-3 trap `{}` exit code drifted\nstdout:\n{}\nstderr:\n{}",
            name,
            String::from_utf8_lossy(&run.stdout),
            String::from_utf8_lossy(&run.stderr)
        );
        assert_eq!(
            String::from_utf8_lossy(&run.stderr),
            expected_stderr,
            "exp-3 trap `{}` stderr drifted",
            name
        );
        assert!(
            run.stdout.is_empty(),
            "exp-3 trap `{}` wrote stdout:\n{}",
            name,
            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-io-host-env-{}-{}-{}.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 temp_root(name: &str) -> PathBuf {
    env::temp_dir().join(format!(
        "glagol-standard-io-host-env-{}-{}-{}",
        name,
        std::process::id(),
        NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
    ))
}

fn slovo_path(path: &Path) -> String {
    path.to_string_lossy()
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
}

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 assert_success_stdout(output: Output, expected: &str, context: &str) {
    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
    );
    assert_eq!(stdout, expected, "{} stdout drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn compile_and_run_with_runtime<F>(clang: &Path, name: &str, ir: &[u8], configure: F) -> Output
where
    F: FnOnce(&mut Command),
{
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    let temp_dir = temp_root("clang");
    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 exp-3 runtime smoke", &clang_output);

    let mut run = Command::new(&exe_path);
    configure(&mut run);
    run.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 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);
}
