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

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

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

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

(test "parse ok max u32"
  (= (std.result.unwrap_ok (std.string.parse_u32_result "4294967295")) 4294967295u32))

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

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

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

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

(test "parse prefix err one"
  (= (std.result.unwrap_err (std.string.parse_u32_result "0x10")) 1))
"#;
    let fixture = write_fixture("boundaries", source);
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string parse u32 boundary tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"parse ok max u32\" ... 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",
            "6 test(s) passed\n",
        ),
        "string parse u32 boundary test stdout drifted"
    );
}

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

(fn main () -> i32
  (std.io.print_u32 (std.result.unwrap_ok (std.string.parse_u32_result "4294967295")))
  (std.result.unwrap_err (std.string.parse_u32_result "+1")))
"#,
    );
    let binary = unique_path("string-parse-u32-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-u32-result build wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "string-parse-u32-result build failed unexpectedly\nstderr:\n{}",
            stderr
        );
        return;
    }

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

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

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

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

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