use std::{
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);

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

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success_stdout(
        formatted,
        &fs::read_to_string(&fixture).expect("read result helpers formatter fixture"),
        "result helpers formatter fixture",
    );

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

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

    let tests = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]);
    assert_success_stdout(
        tests,
        concat!(
            "test \"std result i32 observers\" ... ok\n",
            "test \"std result i32 unwraps\" ... ok\n",
            "test \"std result string observers\" ... ok\n",
            "test \"std result string unwraps\" ... ok\n",
            "test \"legacy result helpers still work\" ... ok\n",
            "5 test(s) passed\n",
        ),
        "result helpers test runner output",
    );
}

#[test]
fn result_helpers_alpha_compiles_without_new_runtime_symbols() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/result-helpers.slo");
    let compile = run_glagol([fixture.as_os_str()]);
    assert_success("compile result helpers fixture", &compile);
    let stdout = String::from_utf8_lossy(&compile.stdout);

    assert!(
        stdout.contains("define i1 @observe_i32_ok()")
            && stdout.contains("define i32 @unwrap_i32_ok()")
            && stdout.contains("define i1 @observe_text_ok()")
            && stdout.contains("define ptr @unwrap_text_ok()")
            && stdout.contains("extractvalue")
            && stdout.contains("__glagol_unwrap_ok_trap")
            && stdout.contains("__glagol_unwrap_err_trap")
            && !stdout.contains("@std.result.is_ok")
            && !stdout.contains("@std.result.is_err")
            && !stdout.contains("@std.result.unwrap_ok")
            && !stdout.contains("@std.result.unwrap_err"),
        "LLVM output did not contain expected result helper lowering shape\nstdout:\n{}",
        stdout
    );
}

#[test]
fn result_helpers_alpha_rejects_deferred_and_misused_std_names() {
    let cases = [
        (
            "map-deferred",
            r#"
(module main)

(fn main () -> i32
  (std.result.map (ok i32 i32 1))
  0)
"#,
            "UnsupportedGenericStandardLibraryCall",
        ),
        (
            "unwrap-or-deferred",
            r#"
(module main)

(fn main () -> i32
  (std.result.unwrap_or (err i32 i32 1) 0))
"#,
            "UnsupportedStandardLibraryCall",
        ),
        (
            "and-then-deferred",
            r#"
(module main)

(fn main () -> i32
  (std.result.and_then (ok i32 i32 1))
  0)
"#,
            "UnsupportedStandardLibraryCall",
        ),
        (
            "observer-non-result",
            r#"
(module main)

(fn main () -> i32
  (if (std.result.is_ok 1) 1 0))
"#,
            "ResultObservationTypeMismatch",
        ),
        (
            "unwrap-non-result",
            r#"
(module main)

(fn main () -> i32
  (std.result.unwrap_ok 1))
"#,
            "ResultUnwrapTypeMismatch",
        ),
        (
            "std-result-helper-shadow",
            r#"
(module main)

(fn std.result.is_ok ((value (result i32 i32))) -> bool
  true)

(fn main () -> i32
  0)
"#,
            "DuplicateFunction",
        ),
    ];

    for (name, source, expected_code) in cases {
        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 `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected result helper diagnostic case `{}` wrote stdout:\n{}",
            name,
            stdout
        );
        assert!(
            stderr.contains(expected_code),
            "diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
            expected_code,
            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 = std::env::temp_dir();
    path.push(format!(
        "glagol-exp15-result-helpers-{}-{}-{}.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
    );
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    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
    );
    assert_eq!(stdout, expected, "{} stdout drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
