use std::{
    env,
    path::{Path, PathBuf},
    process::{Command, Output},
};

#[test]
#[ignore = "runs cargo build and target/debug/glagol; promotion smoke for the built CLI artifact"]
fn cargo_build_produces_direct_binary_that_runs_supported_modes() {
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    let build = Command::new("cargo")
        .arg("build")
        .current_dir(manifest)
        .output()
        .unwrap_or_else(|err| panic!("run cargo build: {}", err));
    assert_success("cargo build", build);

    let binary = direct_debug_binary(manifest);
    assert!(
        binary.is_file(),
        "cargo build did not produce direct binary `{}`",
        binary.display()
    );

    let add_fixture = manifest.join("../examples/add.slo");
    let llvm = Command::new(&binary)
        .arg(&add_fixture)
        .output()
        .unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
    assert_success_contains(
        "direct binary LLVM emission",
        llvm,
        &[
            "define i32 @add",
            "define i32 @main",
            "call void @print_i32",
        ],
    );

    let test_fixture = manifest.join("../tests/top-level-test.slo");
    let check_tests = Command::new(&binary)
        .arg("--check-tests")
        .arg(&test_fixture)
        .output()
        .unwrap_or_else(|err| panic!("run `{}` --check-tests: {}", binary.display(), err));
    assert_success_stdout(
        "direct binary check-tests",
        check_tests,
        "test \"add works\" ... checked\n1 test(s) checked\n",
    );

    let run_tests = Command::new(&binary)
        .arg("--run-tests")
        .arg(&test_fixture)
        .output()
        .unwrap_or_else(|err| panic!("run `{}` --run-tests: {}", binary.display(), err));
    assert_success_stdout(
        "direct binary run-tests",
        run_tests,
        "test \"add works\" ... ok\n1 test(s) passed\n",
    );
}

fn direct_debug_binary(manifest: &Path) -> PathBuf {
    let target = if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR") {
        let target_dir = PathBuf::from(target_dir);
        if target_dir.is_absolute() {
            target_dir
        } else {
            manifest.join(target_dir)
        }
    } else {
        manifest.join("target")
    };

    target.join("debug").join(binary_name())
}

fn binary_name() -> String {
    format!("glagol{}", env::consts::EXE_SUFFIX)
}

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

fn assert_success_contains(context: &str, output: Output, expected: &[&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!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
    for needle in expected {
        assert!(
            stdout.contains(needle),
            "{} stdout did not contain `{}`\nstdout:\n{}",
            context,
            needle,
            stdout
        );
    }
}

fn assert_success_stdout(context: &str, output: Output, expected: &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 mismatch", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
