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 checked_i64_to_i32_fixture_lowers_and_runs_tests() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/checked-i64-to-i32-conversion.slo");

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile checked i64-to-i32 fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("define { i1, i32 } @narrow(i64 %value)")
            && stdout.contains("icmp sge i64 %value, -2147483648")
            && stdout.contains("icmp sle i64 %value, 2147483647")
            && stdout.contains("trunc i64 %value to i32")
            && stdout.contains("select i1 %")
            && stdout.contains("i32 1")
            && stdout.contains("insertvalue { i1, i32 }")
            && !stdout.contains("@std.num.")
            && !stdout.contains("__glagol_i64_to_i32"),
        "checked i64-to-i32 LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run checked i64-to-i32 fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"i64 low bound narrows to i32\" ... ok\n",
            "test \"i64 high bound narrows to i32\" ... ok\n",
            "test \"negative i64 narrows to i32\" ... ok\n",
            "test \"below i32 range returns err\" ... ok\n",
            "test \"above i32 range returns err\" ... ok\n",
            "5 test(s) passed\n",
        ),
        "checked i64-to-i32 test runner stdout drifted"
    );
}

#[test]
fn checked_i64_to_i32_formatter_and_lowering_are_visible() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/checked-i64-to-i32-conversion.slo");

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format checked i64-to-i32 fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(std.num.i64_to_i32_result value)")
            && formatted_stdout.contains("(narrow -2147483648i64)")
            && formatted_stdout.contains("(std.result.unwrap_err value)")
            && formatted_stdout.contains("(above_high_err)"),
        "checked i64-to-i32 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 checked i64-to-i32 surface lowering", &surface);
    assert_eq!(
        String::from_utf8_lossy(&surface.stdout),
        fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/checked-i64-to-i32-conversion.surface.lower")
        )
        .expect("read checked i64-to-i32 surface snapshot"),
        "checked i64-to-i32 surface lowering snapshot drifted"
    );

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

#[test]
fn checked_i64_to_i32_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> (result i32 i32)\n  (std.num.i64_to_i32_result))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> (result i32 i32)\n  (std.num.i64_to_i32_result 1))\n",
            "cannot call `std.num.i64_to_i32_result` with argument of wrong type",
        ),
        (
            "i32-to-i64-result",
            "(module main)\n\n(fn main () -> i32\n  (std.num.i32_to_i64_result 1)\n  0)\n",
            "standard library call `std.num.i32_to_i64_result` is not supported",
        ),
        (
            "cast-checked",
            "(module main)\n\n(fn main () -> i32\n  (std.num.cast_checked 1)\n  0)\n",
            "standard library call `std.num.cast_checked` 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 checked i64-to-i32 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),
            "checked i64-to-i32 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-checked-i64-to-i32-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 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
    );
}
