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

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile f64-to-string fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("declare ptr @__glagol_num_f64_to_string(double)")
            && stdout.contains("call ptr @__glagol_num_f64_to_string(double ")
            && stdout.contains("call void @print_string(ptr %")
            && !stdout.contains("@std.num.f64_to_string"),
        "f64-to-string LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run f64-to-string fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"f64 zero to string\" ... ok\n",
            "test \"f64 fractional to string\" ... ok\n",
            "test \"f64 negative to string\" ... ok\n",
            "test \"f64 whole to string\" ... ok\n",
            "test \"f64 negative string length\" ... ok\n",
            "test \"f64 whole string length\" ... ok\n",
            "6 test(s) passed\n",
        ),
        "f64-to-string test runner stdout drifted"
    );
}

#[test]
fn f64_to_string_hosted_runtime_formats_fixture_values_when_clang_is_available() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/f64-to-string.slo");
    let binary = unique_path("f64-to-string-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 f64-to-string build wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "f64-to-string build failed unexpectedly\nstderr:\n{}",
            stderr
        );
        return;
    }

    let run = Command::new(&binary)
        .output()
        .expect("run f64-to-string binary");
    assert_success("run f64-to-string binary", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "0.0\n3.5\n-1.5\n10.0\n",
        "f64-to-string binary stdout drifted"
    );
}

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

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format f64-to-string fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(std.num.f64_to_string (/ 7.0 2.0))")
            && formatted_stdout.contains("(std.io.print_string (f64_whole_text))"),
        "f64-to-string 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-string 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-string.surface.lower")
        )
        .expect("read f64-to-string surface snapshot"),
        "f64-to-string surface lowering snapshot drifted"
    );

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

#[test]
fn f64_to_string_rejections_are_explicit() {
    for (name, source, expected) in [
        (
            "arity",
            "(module main)\n\n(fn main () -> string\n  (std.num.f64_to_string))\n",
            "wrong number of arguments",
        ),
        (
            "type",
            "(module main)\n\n(fn main () -> string\n  (std.num.f64_to_string 1))\n",
            "cannot call `std.num.f64_to_string` with argument of wrong type",
        ),
        (
            "generic-to-string",
            "(module main)\n\n(fn main () -> i32\n  (std.num.to_string 1.0)\n  0)\n",
            "standard library call `std.num.to_string` is not supported",
        ),
        (
            "f64-parse",
            "(module main)\n\n(fn main () -> i32\n  (std.string.parse_f64 \"1.0\")\n  0)\n",
            "standard library call `std.string.parse_f64` 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-string 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-string 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-string-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
    );
}
