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

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile string-parse-i64-result fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("declare i32 @__glagol_string_parse_i64_result(ptr, ptr)")
            && stdout.contains("define { i1, i64, i32 } @parse_i64(ptr %text)")
            && stdout.contains("alloca i64")
            && stdout.contains("store i64 0, ptr %")
            && stdout.contains("call i32 @__glagol_string_parse_i64_result(ptr %text, ptr %")
            && stdout.contains("load i64, ptr %")
            && stdout.contains("insertvalue { i1, i64, i32 }")
            && stdout.contains("extractvalue { i1, i64, i32 }")
            && !stdout.contains("@std.string.parse_i64_result"),
        "string-parse-i64-result LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string-parse-i64-result fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"parse i64 zero ok\" ... ok\n",
            "test \"parse i64 negative ok\" ... ok\n",
            "test \"parse i64 low ok\" ... ok\n",
            "test \"parse i64 high ok\" ... ok\n",
            "test \"parse i64 empty err\" ... ok\n",
            "test \"parse i64 plus err\" ... ok\n",
            "test \"parse i64 above range err\" ... ok\n",
            "7 test(s) passed\n",
        ),
        "string-parse-i64-result test runner stdout drifted"
    );
}

#[test]
fn string_parse_i64_result_test_runner_covers_boundaries() {
    let source = r#"
(module main)

(test "parse ok min i64"
  (= (std.result.unwrap_ok (std.string.parse_i64_result "-9223372036854775808")) -9223372036854775808i64))

(test "parse ok max i64"
  (= (std.result.unwrap_ok (std.string.parse_i64_result "9223372036854775807")) 9223372036854775807i64))

(test "parse lone minus err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result "-")) 1))

(test "parse leading whitespace err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result " 1")) 1))

(test "parse trailing byte err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result "42x")) 1))

(test "parse underscore err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result "1_0")) 1))

(test "parse prefix err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result "0x10")) 1))

(test "parse underflow err one"
  (= (std.result.unwrap_err (std.string.parse_i64_result "-9223372036854775809")) 1))
"#;
    let fixture = write_fixture("boundaries", source);
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string parse i64 boundary tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"parse ok min i64\" ... ok\n",
            "test \"parse ok max i64\" ... ok\n",
            "test \"parse lone minus err one\" ... ok\n",
            "test \"parse leading whitespace err one\" ... ok\n",
            "test \"parse trailing byte err one\" ... ok\n",
            "test \"parse underscore err one\" ... ok\n",
            "test \"parse prefix err one\" ... ok\n",
            "test \"parse underflow err one\" ... ok\n",
            "8 test(s) passed\n",
        ),
        "string parse i64 boundary test stdout drifted"
    );
}

#[test]
fn string_parse_i64_result_hosted_runtime_parses_bounds_when_clang_is_available() {
    let fixture = write_fixture(
        "runtime-smoke",
        r#"
(module main)

(fn main () -> i32
  (std.io.print_i64 (std.result.unwrap_ok (std.string.parse_i64_result "-9223372036854775808")))
  (std.io.print_i64 (std.result.unwrap_ok (std.string.parse_i64_result "9223372036854775807")))
  (std.result.unwrap_err (std.string.parse_i64_result "+1")))
"#,
    );
    let binary = unique_path("string-parse-i64-result-bin");

    let build = run_glagol([
        OsStr::new("build"),
        fixture.as_os_str(),
        OsStr::new("-o"),
        binary.as_os_str(),
    ]);
    if !build.status.success() {
        let stdout = String::from_utf8_lossy(&build.stdout);
        let stderr = String::from_utf8_lossy(&build.stderr);
        assert!(
            stdout.is_empty(),
            "failed string-parse-i64-result build wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "string-parse-i64-result build failed unexpectedly\nstderr:\n{}",
            stderr
        );
        return;
    }

    let run = Command::new(&binary)
        .output()
        .expect("run string-parse-i64-result binary");
    assert_eq!(
        run.status.code(),
        Some(1),
        "string-parse-i64-result binary 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),
        "-9223372036854775808\n9223372036854775807\n",
        "string-parse-i64-result binary stdout drifted"
    );
    assert!(
        run.stderr.is_empty(),
        "string-parse-i64-result binary wrote stderr:\n{}",
        String::from_utf8_lossy(&run.stderr)
    );
}

#[test]
fn string_parse_i64_result_formatter_and_lowering_are_visible() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/string-parse-i64-result.slo");

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format string-parse-i64-result fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(std.string.parse_i64_result text)")
            && formatted_stdout.contains("(std.result.unwrap_ok (parse_i64 text))")
            && formatted_stdout.contains("(std.result.is_err value)"),
        "string-parse-i64-result formatter output omitted expected calls\nstdout:\n{}",
        formatted_stdout
    );

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

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

#[test]
fn string_parse_i64_result_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> (result i64 i32)\n  (std.string.parse_i64_result))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> (result i64 i32)\n  (std.string.parse_i64_result 1))\n",
            "cannot call `std.string.parse_i64_result` with argument of wrong type",
        ),
        (
            "trap-parse-i64",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_i64 \"42\")\n  0)\n",
            "standard library call `std.string.parse_i64` is not supported",
        ),
        (
            "generic-parse",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_result \"42\")\n  0)\n",
            "standard library call `std.string.parse_result` is not supported",
        ),
        (
            "f64-parse",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_f64 \"1.0\")\n  0)\n",
            "standard library call `std.string.parse_f64` is not supported",
        ),
    ] {
        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 string-parse-i64 rejection fixture `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected compile wrote stdout for `{}`:\n{}",
            name,
            stdout
        );
        assert!(
            stderr.contains(expected),
            "string-parse-i64 diagnostic drifted for `{}`\nstderr:\n{}",
            name,
            stderr
        );
    }
}

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-string-parse-i64-result-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 unique_path(name: &str) -> PathBuf {
    let mut path = env::temp_dir();
    path.push(format!(
        "glagol-{}-{}-{}",
        name,
        std::process::id(),
        NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
    ));
    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
    );
}
