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 \"and true\" ... ok\n",
    "test \"and false\" ... ok\n",
    "test \"or true\" ... ok\n",
    "test \"not false\" ... ok\n",
    "test \"and short circuit\" ... ok\n",
    "test \"or short circuit\" ... ok\n",
    "test \"boolean logic summary\" ... ok\n",
    "7 test(s) passed\n",
);

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

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

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

    let llvm = run_glagol([fixture.as_os_str()]);
    assert_success("compile boolean logic fixture", &llvm);
    let stdout = String::from_utf8_lossy(&llvm.stdout);
    assert!(
        stdout.contains("define i1 @and_true()")
            && stdout.contains("define i1 @and_short_circuits()")
            && stdout.contains("br i1"),
        "boolean logic 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 boolean logic fixture");
}

#[test]
fn boolean_logic_rejections_are_explicit() {
    let bad_and = write_fixture(
        "bad-and-arity",
        "(module main)\n\n(fn main () -> i32\n  (if (and true) 0 1))\n",
    );
    let output = run_glagol([bad_and.as_os_str()]);
    assert_failure_contains(
        &output,
        "bad and arity",
        &[
            "InvalidLogical",
            "logical operator expects exactly two operands",
        ],
    );

    let bad_not = write_fixture(
        "bad-not-arity",
        "(module main)\n\n(fn main () -> i32\n  (if (not true false) 0 1))\n",
    );
    let output = run_glagol([bad_not.as_os_str()]);
    assert_failure_contains(
        &output,
        "bad not arity",
        &["InvalidLogical", "logical not expects exactly one operand"],
    );
}

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
        );
    }
}
