use std::{
    env,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"i32 bit and\" ... ok\n",
    "test \"i32 bit or\" ... ok\n",
    "test \"i32 bit xor\" ... ok\n",
    "test \"i64 bit and\" ... ok\n",
    "test \"i64 bit or\" ... ok\n",
    "test \"i64 bit xor\" ... ok\n",
    "test \"integer bitwise summary\" ... ok\n",
    "7 test(s) passed\n",
);

#[test]
fn integer_bitwise_fixture_formats_lowers_and_runs() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/integer-bitwise.slo");

    let fmt = run_glagol([
        OsStr::new("fmt"),
        OsStr::new("--check"),
        fixture.as_os_str(),
    ]);
    assert_success("format integer bitwise fixture", &fmt);

    let check = run_glagol([OsStr::new("check"), fixture.as_os_str()]);
    assert_success_stdout(check, "", "check integer bitwise fixture");

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile integer bitwise fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("and i32")
            && stdout.contains("or i32")
            && stdout.contains("xor i32")
            && stdout.contains("and i64")
            && stdout.contains("or i64")
            && stdout.contains("xor i64"),
        "integer bitwise LLVM shape drifted\nstdout:\n{}",
        stdout
    );

    let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success_stdout(tests, EXPECTED_TEST_OUTPUT, "test integer bitwise fixture");
}

#[test]
fn integer_bitwise_rejections_are_explicit() {
    let f64_fixture = write_fixture(
        "f64-bitwise",
        "(module main)\n\n(fn main () -> i32\n  (if (= (bit_and 5.0 2.0) 0.0) 0 1))\n",
    );
    let f64_output = run_glagol([f64_fixture.as_os_str()]);
    assert_failure_contains(
        &f64_output,
        "f64 bitwise rejection",
        &[
            "UnsupportedF64Bitwise",
            "floating-point bitwise operations are not supported",
            "f64 bitwise operations remain deferred",
        ],
    );
}

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-{}-{}-{}.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
    );
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    assert_success(context, &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        expected,
        "{} stdout drifted",
        context
    );
}

fn assert_failure_contains(output: &Output, context: &str, expected: &[&str]) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert!(
        stdout.is_empty(),
        "{} wrote stdout on failure:\n{}",
        context,
        stdout
    );
    for needle in expected {
        assert!(
            stderr.contains(needle),
            "{} stderr missing `{}`\nstderr:\n{}",
            context,
            needle,
            stderr
        );
    }
}
