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 BOOL_LOCAL_FIXTURE: &str = r#"(module main)

(fn choose ((flag bool)) -> i32
  (let saved bool flag)
  (if saved
    42
    7))

(fn choose_from_compare ((value i32)) -> i32
  (let negative bool (< value 0))
  (if negative
    1
    0))

(test "immutable bool local from param"
  (= (choose true) 42))

(test "immutable bool local false branch"
  (= (choose false) 7))

(test "immutable bool local from compare"
  (= (choose_from_compare -4) 1))

(fn main () -> i32
  (+ (choose true) (choose_from_compare -4)))"#;

#[test]
fn immutable_bool_let_locals_compile_lower_and_test() {
    let fixture = write_fixture("immutable-bool-local", BOOL_LOCAL_FIXTURE);

    let compile = run_glagol([fixture.as_os_str()]);
    let llvm_stdout = String::from_utf8_lossy(&compile.stdout);
    let llvm_stderr = String::from_utf8_lossy(&compile.stderr);
    assert!(
        compile.status.success(),
        "compiler rejected immutable bool local fixture\nstdout:\n{}\nstderr:\n{}",
        llvm_stdout,
        llvm_stderr
    );
    assert!(
        llvm_stdout.contains("define i32 @choose(i1 %flag)")
            && llvm_stdout.contains("define i32 @choose_from_compare(i32 %value)")
            && llvm_stdout.contains("icmp slt i32 %value, 0")
            && llvm_stdout.contains("br i1"),
        "LLVM output lost immutable bool local shape\nstdout:\n{}",
        llvm_stdout
    );
    assert!(
        llvm_stderr.is_empty(),
        "compiler wrote stderr:\n{}",
        llvm_stderr
    );

    let tests = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]);
    assert_success_stdout(
        tests,
        concat!(
            "test \"immutable bool local from param\" ... ok\n",
            "test \"immutable bool local false branch\" ... ok\n",
            "test \"immutable bool local from compare\" ... ok\n",
            "3 test(s) passed\n",
        ),
        "immutable bool local test runner output",
    );

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success_stdout(
        formatted,
        &format!("{}\n", BOOL_LOCAL_FIXTURE),
        "immutable bool local formatter output",
    );

    let surface = run_glagol([
        OsStr::new("--inspect-lowering=surface"),
        fixture.as_os_str(),
    ]);
    let surface_stdout = String::from_utf8_lossy(&surface.stdout);
    let surface_stderr = String::from_utf8_lossy(&surface.stderr);
    assert!(
        surface.status.success(),
        "surface lowering rejected immutable bool local fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("local let saved: bool")
            && surface_stdout.contains("local let negative: bool")
            && surface_stdout.contains("var saved")
            && surface_stdout.contains("var negative"),
        "surface lowering lost immutable bool local declarations\nstdout:\n{}",
        surface_stdout
    );
    assert!(
        surface_stderr.is_empty(),
        "surface lowering wrote stderr:\n{}",
        surface_stderr
    );

    let checked = run_glagol([
        OsStr::new("--inspect-lowering=checked"),
        fixture.as_os_str(),
    ]);
    let checked_stdout = String::from_utf8_lossy(&checked.stdout);
    let checked_stderr = String::from_utf8_lossy(&checked.stderr);
    assert!(
        checked.status.success(),
        "checked lowering rejected immutable bool local fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("local let saved : unit")
            && checked_stdout.contains("local let negative : unit")
            && checked_stdout.contains("var saved : bool")
            && checked_stdout.contains("var negative : bool"),
        "checked lowering lost typed immutable bool local flow\nstdout:\n{}",
        checked_stdout
    );
    assert!(
        checked_stderr.is_empty(),
        "checked lowering wrote stderr:\n{}",
        checked_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-immutable-bool-locals-{}-{}-{}.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_stdout(output: Output, expected: &str, context: &str) {
    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_eq!(stdout, expected, "{} stdout drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
