use std::collections::HashMap;
use std::env;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};

pub static ENV_MUTEX: Mutex<()> = Mutex::new(());

pub fn lock_env() -> MutexGuard<'static, ()> {
    ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
}

#[must_use]
pub struct TestProcess {
    _lock: MutexGuard<'static, ()>,
    original_cwd: PathBuf,
    original_vars: HashMap<OsString, Option<OsString>>,
}

impl TestProcess {
    pub fn new() -> Self {
        let lock = lock_env();
        let original_cwd = env::current_dir().expect("current dir");
        Self {
            _lock: lock,
            original_cwd,
            original_vars: HashMap::new(),
        }
    }

    pub fn chdir(&mut self, path: impl AsRef<Path>) -> std::io::Result<()> {
        env::set_current_dir(path)
    }

    pub fn set_var(&mut self, key: impl Into<OsString>, value: impl AsRef<OsStr>) {
        let key = key.into();
        self.remember_var(&key);
        unsafe {
            env::set_var(&key, value);
        }
    }

    pub fn remove_var(&mut self, key: impl Into<OsString>) {
        let key = key.into();
        self.remember_var(&key);
        unsafe {
            env::remove_var(&key);
        }
    }

    fn remember_var(&mut self, key: &OsStr) {
        if self.original_vars.contains_key(key) {
            return;
        }
        self.original_vars
            .insert(key.to_os_string(), env::var_os(key));
    }
}

impl Default for TestProcess {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for TestProcess {
    fn drop(&mut self) {
        for (key, previous) in self.original_vars.drain() {
            if let Some(value) = previous {
                unsafe {
                    env::set_var(&key, value);
                }
            } else {
                unsafe {
                    env::remove_var(&key);
                }
            }
        }
        let _ = env::set_current_dir(&self.original_cwd);
    }
}
