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

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

const EXPECTED_OUTPUT: &str =
    "test \"installed stdlib source discovery\" ... ok\n1 test(s) passed\n";

#[test]
fn copied_binary_discovers_installed_share_slovo_std() {
    let root = temp_root();
    let bin_dir = root.join("bin");
    let std_dir = root.join("share/slovo/std");
    let project = root.join("project");
    let source_dir = project.join("src");
    let installed_glagol = bin_dir.join("glagol");

    create_dir(&bin_dir);
    create_dir(&std_dir);
    create_dir(&source_dir);

    fs::copy(env!("CARGO_BIN_EXE_glagol"), &installed_glagol).unwrap_or_else(|err| {
        panic!(
            "copy glagol binary to `{}` failed: {}",
            installed_glagol.display(),
            err
        )
    });
    make_executable(&installed_glagol);

    write(
        &std_dir.join("installed_probe.slo"),
        "(module installed_probe (export value))\n\n(fn value () -> i32\n  42)\n",
    );
    write(
        &project.join("slovo.toml"),
        concat!(
            "[project]\n",
            "name = \"installed-stdlib-discovery-alpha\"\n",
            "source_root = \"src\"\n",
            "entry = \"main\"\n",
        ),
    );
    write(
        &source_dir.join("main.slo"),
        concat!(
            "(module main)\n\n",
            "(import std.installed_probe (value))\n\n",
            "(fn main () -> i32\n",
            "  (value))\n\n",
            "(test \"installed stdlib source discovery\"\n",
            "  (= (value) 42))\n",
        ),
    );

    assert!(
        !project.join("src/installed_probe.slo").exists(),
        "fixture must not carry a local installed_probe module copy"
    );

    let check = run_installed_glagol(
        &installed_glagol,
        [OsStr::new("check"), project.as_os_str()],
        &root,
    );
    assert_success_stdout(check, "", "installed stdlib discovery check");

    let test = run_installed_glagol(
        &installed_glagol,
        [OsStr::new("test"), project.as_os_str()],
        &root,
    );
    assert_success_stdout(test, EXPECTED_OUTPUT, "installed stdlib discovery test");
}

fn run_installed_glagol<I, S>(binary: &Path, args: I, cwd: &Path) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    Command::new(binary)
        .args(args)
        .current_dir(cwd)
        .env_remove("SLOVO_STD_PATH")
        .output()
        .expect("run installed glagol")
}

fn write(path: &Path, content: &str) {
    fs::write(path, content).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
}

fn create_dir(path: &Path) {
    fs::create_dir_all(path).unwrap_or_else(|err| panic!("create `{}`: {}", path.display(), err));
}

#[cfg(unix)]
fn make_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;

    let mut permissions = fs::metadata(path)
        .unwrap_or_else(|err| panic!("metadata `{}`: {}", path.display(), err))
        .permissions();
    permissions.set_mode(permissions.mode() | 0o755);
    fs::set_permissions(path, permissions)
        .unwrap_or_else(|err| panic!("chmod `{}`: {}", path.display(), err));
}

#[cfg(not(unix))]
fn make_executable(_path: &Path) {}

fn temp_root() -> std::path::PathBuf {
    let path = env::temp_dir().join(format!(
        "glagol-installed-stdlib-discovery-alpha-{}-{}",
        std::process::id(),
        NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
    ));
    let _ = fs::remove_dir_all(&path);
    fs::create_dir_all(&path).unwrap_or_else(|err| panic!("create `{}`: {}", 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\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
        context,
        output.status.code(),
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "{}", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
