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 numeric_widening_fixture_lowers_and_runs_tests() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/numeric-widening-conversions.slo");

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile numeric widening fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("define i64 @base_i64()")
            && stdout.contains("sext i32 2147483647 to i64")
            && stdout.contains("sitofp i32 5 to double")
            && stdout.contains("sitofp i64 %")
            && stdout.contains("call void @print_i64(i64")
            && stdout.contains("call void @print_f64(double")
            && !stdout.contains("@std.num."),
        "numeric widening LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run numeric widening fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"i32 widens to i64\" ... ok\n",
            "test \"i32 to i64 feeds i64 arithmetic\" ... ok\n",
            "test \"i32 widens to f64\" ... ok\n",
            "test \"i64 widens to f64\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "numeric widening test runner stdout drifted"
    );
}

#[test]
fn numeric_widening_formatter_and_lowering_are_visible() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/numeric-widening-conversions.slo");

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format numeric widening fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(std.num.i32_to_i64 2147483647)")
            && formatted_stdout.contains("(std.num.i32_to_f64 5)")
            && formatted_stdout.contains("(std.num.i64_to_f64 (widened_i64))"),
        "numeric widening 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 numeric widening surface lowering", &surface);
    assert_eq!(
        String::from_utf8_lossy(&surface.stdout),
        fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/numeric-widening-conversions.surface.lower")
        )
        .expect("read numeric widening surface snapshot"),
        "numeric widening surface lowering snapshot drifted"
    );

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

#[test]
fn numeric_widening_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> i64\n  (std.num.i32_to_i64))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> f64\n  (std.num.i32_to_f64 1i64))\n",
            "cannot call `std.num.i32_to_f64` with argument of wrong type",
        ),
        (
            "i64-to-i32",
            "(module main)\n\n(fn main () -> i32\n  (std.num.i64_to_i32 1i64)\n  0)\n",
            "standard library call `std.num.i64_to_i32` is not supported",
        ),
        (
            "f64-to-i32",
            "(module main)\n\n(fn main () -> i32\n  (std.num.f64_to_i32 1.0)\n  0)\n",
            "standard library call `std.num.f64_to_i32` is not supported",
        ),
        (
            "cast",
            "(module main)\n\n(fn main () -> i32\n  (std.num.cast 1)\n  0)\n",
            "standard library call `std.num.cast` 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 numeric widening 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),
            "numeric widening 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-numeric-widening-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
    );
}
