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

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile u32 fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("declare void @print_u32(i32)")
            && stdout.contains("define i32 @base()")
            && stdout.contains("add i32")
            && stdout.contains("mul i32")
            && stdout.contains("icmp ugt i32")
            && stdout.contains("icmp ult i32")
            && stdout.contains("icmp eq i32")
            && stdout.contains("call void @print_u32(i32")
            && !stdout.contains("@std.io.print_u32"),
        "u32 LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run u32 fixture tests", &tests);
    assert_eq!(
        String::from_utf8_lossy(&tests.stdout),
        concat!(
            "test \"u32 arithmetic returns exact fixture value\" ... ok\n",
            "test \"u32 comparison works in predicates\" ... ok\n",
            "test \"u32 division and ordering\" ... ok\n",
            "3 test(s) passed\n",
        ),
        "u32 test runner stdout drifted"
    );
}

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

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success("format u32 fixture", &formatted);
    let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
    assert!(
        formatted_stdout.contains("(fn base () -> u32")
            && formatted_stdout.contains("1073741824u32")
            && formatted_stdout.contains("(std.io.print_u32 (local_total))"),
        "u32 formatter output omitted expected forms\nstdout:\n{}",
        formatted_stdout
    );

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

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

#[test]
fn mixed_u32_operands_are_rejected_clearly() {
    for (name, expression, found) in [
        ("mixed-u32-i32", "(= 1u32 1)", "u32 and i32"),
        ("mixed-u32-u64", "(= 1u32 1u64)", "u32 and u64"),
        ("mixed-u32-f64", "(= 1u32 1.0)", "u32 and f64"),
    ] {
        let fixture = write_fixture(
            name,
            &format!(
                "(module main)\n\n(fn main () -> i32\n  (if {} 0 1))\n",
                expression
            ),
        );
        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 mixed u32 operands\nstdout:\n{}\nstderr:\n{}",
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected compile wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains("numeric operands must have the same primitive type")
                && stderr.contains("mixed i32/i64/u32/u64/f64")
                && stderr.contains(found),
            "mixed numeric diagnostic drifted for {}\nstderr:\n{}",
            name,
            stderr
        );
    }
}

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

    let run = Command::new(&binary)
        .output()
        .expect("run u32 numeric primitive binary");
    assert_success("run u32 numeric primitive binary", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "2147483663\n",
        "u32 runtime print stdout drifted"
    );
    assert!(
        run.stderr.is_empty(),
        "u32 runtime print wrote stderr:\n{}",
        String::from_utf8_lossy(&run.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-u32-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
    );
}
