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

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

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string-parse-f64-result fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"parse f64 decimal ok\" ... ok\n",
            "test \"parse f64 negative decimal ok\" ... ok\n",
            "test \"parse f64 text err\" ... ok\n",
            "test \"parse f64 nan err\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "string-parse-f64-result test runner stdout drifted"
    );
}

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

(test "parse f64 decimal ok"
  (= (std.result.unwrap_ok (std.string.parse_f64_result "3.5")) 3.5))

(test "parse f64 negative decimal ok"
  (= (std.result.unwrap_ok (std.string.parse_f64_result "-3.5")) (- 0.0 3.5)))

(test "parse f64 signed plus err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "+3.5")) 1))

(test "parse f64 leading dot err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result ".5")) 1))

(test "parse f64 trailing dot err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "5.")) 1))

(test "parse f64 exponent err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "1e2")) 1))

(test "parse f64 nan err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "nan")) 1))

(test "parse f64 infinity err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "inf")) 1))

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

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

(test "parse f64 hex err one"
  (= (std.result.unwrap_err (std.string.parse_f64_result "0x1.0p0")) 1))
"#;
    let fixture = write_fixture("boundaries", source);
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string parse f64 boundary tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"parse f64 decimal ok\" ... ok\n",
            "test \"parse f64 negative decimal ok\" ... ok\n",
            "test \"parse f64 signed plus err one\" ... ok\n",
            "test \"parse f64 leading dot err one\" ... ok\n",
            "test \"parse f64 trailing dot err one\" ... ok\n",
            "test \"parse f64 exponent err one\" ... ok\n",
            "test \"parse f64 nan err one\" ... ok\n",
            "test \"parse f64 infinity err one\" ... ok\n",
            "test \"parse f64 leading whitespace err one\" ... ok\n",
            "test \"parse f64 underscore err one\" ... ok\n",
            "test \"parse f64 hex err one\" ... ok\n",
            "11 test(s) passed\n",
        ),
        "string parse f64 boundary test stdout drifted"
    );
}

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

(fn main () -> i32
  (std.io.print_string (std.num.f64_to_string (std.result.unwrap_ok (std.string.parse_f64_result "-0.5"))))
  (std.io.print_string (std.num.f64_to_string (std.result.unwrap_ok (std.string.parse_f64_result "12.5"))))
  (std.result.unwrap_err (std.string.parse_f64_result "inf")))
"#,
    );
    let binary = unique_path("string-parse-f64-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-f64-result build wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "string-parse-f64-result build failed unexpectedly\nstderr:\n{}",
            stderr
        );
        return;
    }

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

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

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format string-parse-f64-result fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(std.string.parse_f64_result text)")
            && formatted_stdout.contains("(std.result.unwrap_ok value)")
            && formatted_stdout.contains("(std.result.is_err value)"),
        "string-parse-f64-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-f64-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-f64-result.surface.lower")
        )
        .expect("read string-parse-f64-result surface snapshot"),
        "string-parse-f64-result surface lowering snapshot drifted"
    );

    let checked = run_glagol([
        OsStr::new("--inspect-lowering=checked"),
        fixture.as_os_str(),
    ]);
    assert_success("inspect string-parse-f64-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-f64-result.checked.lower")
        )
        .expect("read string-parse-f64-result checked snapshot"),
        "string-parse-f64-result checked lowering snapshot drifted"
    );
}

#[test]
fn string_parse_f64_result_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> (result f64 i32)\n  (std.string.parse_f64_result))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> (result f64 i32)\n  (std.string.parse_f64_result 1))\n",
            "cannot call `std.string.parse_f64_result` with argument of wrong type",
        ),
        (
            "trap-parse-f64",
            "(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",
        ),
        (
            "generic-parse",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_result \"1.0\")\n  0)\n",
            "standard library call `std.string.parse_result` 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-f64 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-f64 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-f64-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
    );
}
