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

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

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string-parse-bool-result fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"parse bool true ok\" ... ok\n",
            "test \"parse bool false ok\" ... ok\n",
            "test \"parse bool uppercase err\" ... ok\n",
            "test \"parse bool empty err\" ... ok\n",
            "test \"parse bool whitespace err\" ... ok\n",
            "test \"parse bool helper flow\" ... ok\n",
            "6 test(s) passed\n",
        ),
        "string-parse-bool-result test runner stdout drifted"
    );
}

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

(test "parse true ok"
  (std.result.unwrap_ok (std.string.parse_bool_result "true")))

(test "parse false ok"
  (if (std.result.unwrap_ok (std.string.parse_bool_result "false"))
    false
    true))

(test "parse empty err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "")) 1))

(test "parse uppercase err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "TRUE")) 1))

(test "parse mixed case err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "False")) 1))

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

(test "parse trailing whitespace err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "false ")) 1))

(test "parse numeric err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "1")) 1))

(test "parse other text err one"
  (= (std.result.unwrap_err (std.string.parse_bool_result "truth")) 1))
"#;
    let fixture = write_fixture("boundaries", source);
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run string parse bool boundary tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"parse true ok\" ... ok\n",
            "test \"parse false ok\" ... ok\n",
            "test \"parse empty err one\" ... ok\n",
            "test \"parse uppercase err one\" ... ok\n",
            "test \"parse mixed case err one\" ... ok\n",
            "test \"parse leading whitespace err one\" ... ok\n",
            "test \"parse trailing whitespace err one\" ... ok\n",
            "test \"parse numeric err one\" ... ok\n",
            "test \"parse other text err one\" ... ok\n",
            "9 test(s) passed\n",
        ),
        "string parse bool boundary test stdout drifted"
    );
}

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

(fn main () -> i32
  (std.io.print_bool (std.result.unwrap_ok (std.string.parse_bool_result "true")))
  (std.io.print_bool (std.result.unwrap_ok (std.string.parse_bool_result "false")))
  (std.result.unwrap_err (std.string.parse_bool_result "TRUE")))
"#,
    );
    let binary = unique_path("string-parse-bool-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-bool-result build wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "string-parse-bool-result build failed unexpectedly\nstderr:\n{}",
            stderr
        );
        return;
    }

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

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

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

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

#[test]
fn string_parse_bool_result_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> (result bool i32)\n  (std.string.parse_bool_result))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> (result bool i32)\n  (std.string.parse_bool_result 1))\n",
            "cannot call `std.string.parse_bool_result` with argument of wrong type",
        ),
        (
            "trap-parse-bool",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_bool \"true\")\n  0)\n",
            "standard library call `std.string.parse_bool` is not supported",
        ),
        (
            "generic-parse",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_result \"true\")\n  0)\n",
            "standard library call `std.string.parse_result` is not supported",
        ),
        (
            "deferred-code",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_bool_code_result \"true\")\n  0)\n",
            "standard library call `std.string.parse_bool_code_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-bool 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-bool 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-bool-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
    );
}
