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_string_scanning_beta16_lowering_shape_uses_runtime_symbols() {
    let fixture = write_fixture(
        "std-string-scanning-beta16-lowering",
        r#"
(module main)

(fn byte_or_code ((text string) (index i32)) -> i32
  (match (std.string.byte_at_result text index)
    ((ok value)
      value)
    ((err code)
      code)))

(fn slice_or_empty ((text string) (start i32) (count i32)) -> string
  (match (std.string.slice_result text start count)
    ((ok value)
      value)
    ((err code)
      "")))

(fn has_edges ((text string)) -> bool
  (if (std.string.starts_with text "slo")
    (std.string.ends_with text "vo")
    false))

(fn main () -> i32
  (if (has_edges (slice_or_empty "slovo" 0 5))
    (byte_or_code "slovo" 3)
    1))
"#,
    );
    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 rejected beta16 string scanning fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("declare i64 @__glagol_string_byte_at_result(ptr, i32)")
            && stdout.contains("declare ptr @__glagol_string_slice_result(ptr, i32, i32)")
            && stdout.contains("declare i1 @__glagol_string_starts_with(ptr, ptr)")
            && stdout.contains("declare i1 @__glagol_string_ends_with(ptr, ptr)")
            && stdout.contains("call i64 @__glagol_string_byte_at_result(ptr %text, i32 %index)")
            && stdout.contains(
                "call ptr @__glagol_string_slice_result(ptr %text, i32 %start, i32 %count)"
            )
            && stdout.contains("call i1 @__glagol_string_starts_with(ptr %text, ptr @.str.")
            && stdout.contains("call i1 @__glagol_string_ends_with(ptr %text, ptr @.str.")
            && stdout.contains("icmp ne ptr %")
            && stdout.contains("insertvalue { i1, ptr, i32 }")
            && !stdout.contains("@std.string.byte_at_result")
            && !stdout.contains("@std.string.slice_result")
            && !stdout.contains("@std.string.starts_with")
            && !stdout.contains("@std.string.ends_with"),
        "LLVM output did not contain expected beta16 string runtime shape\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}

#[test]
fn test_runner_executes_standard_string_scanning_beta16_boundaries() {
    let fixture = write_fixture(
        "std-string-scanning-beta16-test-runner",
        r#"
(module main)

(test "byte first ok"
  (= (unwrap_ok (std.string.byte_at_result "slovo" 0)) 115))

(test "byte last ok"
  (= (unwrap_ok (std.string.byte_at_result "slovo" 4)) 111))

(test "byte empty err"
  (= (unwrap_err (std.string.byte_at_result "" 0)) 1))

(test "byte negative err"
  (= (unwrap_err (std.string.byte_at_result "slovo" -1)) 1))

(test "byte end err"
  (= (unwrap_err (std.string.byte_at_result "slovo" 5)) 1))

(test "slice middle ok"
  (= (unwrap_ok (std.string.slice_result "slovo" 1 3)) "lov"))

(test "slice zero count ok"
  (= (unwrap_ok (std.string.slice_result "slovo" 2 0)) ""))

(test "slice len zero ok"
  (= (unwrap_ok (std.string.slice_result "slovo" 5 0)) ""))

(test "slice full ok"
  (= (unwrap_ok (std.string.slice_result "slovo" 0 5)) "slovo"))

(test "slice negative start err"
  (= (unwrap_err (std.string.slice_result "slovo" -1 1)) 1))

(test "slice negative count err"
  (= (unwrap_err (std.string.slice_result "slovo" 1 -1)) 1))

(test "slice overrun err"
  (= (unwrap_err (std.string.slice_result "slovo" 4 2)) 1))

(test "slice start past end err"
  (= (unwrap_err (std.string.slice_result "slovo" 6 0)) 1))

(test "starts true empty"
  (if (std.string.starts_with "slovo" "slo")
    (std.string.starts_with "slovo" "")
    false))

(test "starts false middle"
  (= (std.string.starts_with "slovo" "ovo") false))

(test "ends true empty"
  (if (std.string.ends_with "slovo" "ovo")
    (std.string.ends_with "slovo" "")
    false))

(test "ends false prefix"
  (= (std.string.ends_with "slovo" "slo") false))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run beta16 string scanning tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"byte first ok\" ... ok\n",
            "test \"byte last ok\" ... ok\n",
            "test \"byte empty err\" ... ok\n",
            "test \"byte negative err\" ... ok\n",
            "test \"byte end err\" ... ok\n",
            "test \"slice middle ok\" ... ok\n",
            "test \"slice zero count ok\" ... ok\n",
            "test \"slice len zero ok\" ... ok\n",
            "test \"slice full ok\" ... ok\n",
            "test \"slice negative start err\" ... ok\n",
            "test \"slice negative count err\" ... ok\n",
            "test \"slice overrun err\" ... ok\n",
            "test \"slice start past end err\" ... ok\n",
            "test \"starts true empty\" ... ok\n",
            "test \"starts false middle\" ... ok\n",
            "test \"ends true empty\" ... ok\n",
            "test \"ends false prefix\" ... ok\n",
            "17 test(s) passed\n",
        ),
        "beta16 string scanning test runner stdout drifted"
    );
}

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

    let fixture = write_fixture(
        "std-string-scanning-beta16-runtime-smoke",
        r#"
(module main)

(fn main () -> i32
  (std.io.print_i32 (unwrap_ok (std.string.byte_at_result "slovo" 3)))
  (std.io.print_string (unwrap_ok (std.string.slice_result "slovo" 1 3)))
  (std.io.print_bool (std.string.starts_with "slovo" "slo"))
  (std.io.print_bool (std.string.ends_with "slovo" "ovo"))
  (std.io.print_i32 (unwrap_err (std.string.byte_at_result "slovo" 5)))
  (std.io.print_i32 (unwrap_err (std.string.slice_result "slovo" 4 2)))
  0)
"#,
    );
    let compile = run_glagol([fixture.as_os_str()]);
    assert_success("compile beta16 string scanning runtime smoke", &compile);

    let run = compile_and_run_with_runtime(&clang, "std-string-scanning-beta16", &compile.stdout);
    assert_success("run beta16 string scanning runtime smoke", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "118\nlov\ntrue\ntrue\n1\n1\n",
        "beta16 string scanning runtime 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-standard-string-scanning-beta16-{}-{}-{}.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-string-scanning-beta16-{}-{}",
        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 beta16 string scanning 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);
}
