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

(fn flip_flag ((flag bool)) -> bool
  (var current bool flag)
  (set current (if current
    false
    true))
  current)

(fn add_wide_local ((base i64)) -> i64
  (var count i64 base)
  (set count (+ count 2i64))
  count)

(fn add_ratio_local ((base f64)) -> f64
  (var amount f64 base)
  (set amount (+ amount 0.5))
  amount)

(test "mutable bool local true branch"
  (if (flip_flag true)
    false
    true))

(test "mutable bool local false branch"
  (flip_flag false))

(test "mutable i64 local"
  (= (add_wide_local 40i64) 42i64))

(test "mutable f64 local"
  (= (add_ratio_local 41.5) 42.0))

(fn main () -> i32
  0)"#;

#[test]
fn mutable_scalar_locals_compile_lower_and_test() {
    let fixture = write_fixture("mutable-scalar-local", MUTABLE_SCALAR_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 mutable scalar local fixture\nstdout:\n{}\nstderr:\n{}",
        llvm_stdout,
        llvm_stderr
    );
    assert!(
        llvm_stdout.contains("define i1 @flip_flag(i1 %flag)")
            && llvm_stdout.contains("%current.addr = alloca i1")
            && llvm_stdout.contains("define i64 @add_wide_local(i64 %base)")
            && llvm_stdout.contains("%count.addr = alloca i64")
            && llvm_stdout.contains("define double @add_ratio_local(double %base)")
            && llvm_stdout.contains("%amount.addr = alloca double")
            && llvm_stdout.contains("store i1")
            && llvm_stdout.contains("store i64")
            && llvm_stdout.contains("store double"),
        "LLVM output lost mutable scalar 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 \"mutable bool local true branch\" ... ok\n",
            "test \"mutable bool local false branch\" ... ok\n",
            "test \"mutable i64 local\" ... ok\n",
            "test \"mutable f64 local\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "mutable scalar local test runner output",
    );

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success_stdout(
        formatted,
        &format!("{}\n", MUTABLE_SCALAR_FIXTURE),
        "mutable scalar 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 mutable scalar local fixture\nstdout:\n{}\nstderr:\n{}",
        surface_stdout,
        surface_stderr
    );
    assert!(
        surface_stdout.contains("local var current: bool")
            && surface_stdout.contains("set current")
            && surface_stdout.contains("local var count: i64")
            && surface_stdout.contains("set count")
            && surface_stdout.contains("local var amount: f64")
            && surface_stdout.contains("set amount"),
        "surface lowering lost mutable scalar 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 mutable scalar local fixture\nstdout:\n{}\nstderr:\n{}",
        checked_stdout,
        checked_stderr
    );
    assert!(
        checked_stdout.contains("local var current : unit")
            && checked_stdout.contains("set current : unit")
            && checked_stdout.contains("var current : bool")
            && checked_stdout.contains("local var count : unit")
            && checked_stdout.contains("set count : unit")
            && checked_stdout.contains("var count : i64")
            && checked_stdout.contains("local var amount : unit")
            && checked_stdout.contains("set amount : unit")
            && checked_stdout.contains("var amount : f64"),
        "checked lowering lost typed mutable scalar 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-mutable-scalar-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);
}
