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

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile f64-to-i32-result fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("fcmp oge double")
            && stdout.contains("-2147483648.0")
            && stdout.contains("fcmp ole double")
            && stdout.contains("2147483647.0")
            && stdout.contains("br i1 %")
            && stdout.contains("f64.to_i32.integral")
            && stdout.contains("f64.to_i32.exponent")
            && stdout.contains("f64.to_i32.fraction")
            && stdout.contains("bitcast double")
            && stdout.contains("lshr i64")
            && stdout.contains("fptosi double")
            && !stdout.contains("frem double")
            && stdout.contains("phi { i1, i32 }")
            && !stdout.contains("@std.num.f64_to_i32_result")
            && !stdout.contains("__glagol_num_f64_to_i32_result"),
        "f64-to-i32-result LLVM shape drifted\nstdout:\n{}",
        stdout
    );
    let integral_pos = stdout
        .find("f64.to_i32.integral")
        .expect("integral check block is present");
    let fraction_pos = stdout
        .find("f64.to_i32.fraction")
        .expect("fraction bit check block is present");
    let fptosi_pos = stdout
        .find("fptosi double")
        .expect("narrowing conversion is present");
    assert!(
        integral_pos < fraction_pos && fraction_pos < fptosi_pos,
        "f64-to-i32-result must prove integrality before fptosi\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run f64-to-i32-result fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"f64 zero narrows to i32\" ... ok\n",
            "test \"negative f64 narrows to i32\" ... ok\n",
            "test \"fractional f64 returns err\" ... ok\n",
            "test \"above i32 range f64 returns err\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "f64-to-i32-result test runner stdout drifted"
    );
}

#[test]
fn f64_to_i32_result_non_finite_returns_err_in_test_runner() {
    let fixture = write_fixture(
        "non-finite",
        r#"
(module main)

(fn main () -> i32
  0)

(test "infinity returns err"
  (let value (result i32 i32) (std.num.f64_to_i32_result (/ 1.0 0.0)))
  (if (std.result.is_err value)
    (= (std.result.unwrap_err value) 1)
    false))
"#,
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run f64-to-i32-result non-finite test", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"infinity returns err\" ... ok\n",
            "1 test(s) passed\n",
        ),
        "f64-to-i32-result non-finite test runner stdout drifted"
    );
}

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

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

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

#[test]
fn f64_to_i32_result_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> (result i32 i32)\n  (std.num.f64_to_i32_result))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> (result i32 i32)\n  (std.num.f64_to_i32_result 1))\n",
            "cannot call `std.num.f64_to_i32_result` with argument of wrong type",
        ),
        (
            "context",
            "(module main)\n\n(fn main () -> i32\n  (std.num.f64_to_i32_result 1.0))\n",
            "function `main` returns wrong type",
        ),
        (
            "unchecked",
            "(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-checked",
            "(module main)\n\n(fn main () -> i32\n  (std.num.cast_checked 1.0)\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 f64-to-i32-result 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),
            "f64-to-i32-result 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-f64-to-i32-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 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
    );
}
