//! ## ConfigClient
//!
//! `config_client` is the module which provides an API between the Config module and the system

// Locals
// Ext
use std::fs::{File, OpenOptions, create_dir, remove_file};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;

use crate::config::params::{DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD, UserConfig};
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol;

// Types
pub type SshHost = (String, String, PathBuf); // 0: host, 1: username, 2: RSA key path

/// ConfigClient provides a high level API to communicate with the termscp configuration
pub struct ConfigClient {
    config: UserConfig,   // Configuration loaded
    config_path: PathBuf, // Configuration TOML Path
    ssh_key_dir: PathBuf, // SSH Key storage directory
    degraded: bool,       // Indicates the `ConfigClient` is working in degraded mode
}

impl ConfigClient {
    /// Instantiate a new `ConfigClient` with provided path
    pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<Self, SerializerError> {
        // Initialize a default configuration
        let default_config: UserConfig = UserConfig::default();
        info!(
            "Setting up config client with config path {} and SSH key directory {}",
            config_path.display(),
            ssh_key_dir.display()
        );
        // Create client
        let mut client: ConfigClient = ConfigClient {
            config: default_config,
            config_path: PathBuf::from(config_path),
            ssh_key_dir: PathBuf::from(ssh_key_dir),
            degraded: false,
        };
        // If ssh key directory doesn't exist, create it
        if !ssh_key_dir.exists() {
            if let Err(err) = create_dir(ssh_key_dir) {
                error!("Failed to create SSH key dir: {}", err);
                return Err(SerializerError::new_ex(
                    SerializerErrorKind::Io,
                    format!(
                        "Could not create SSH key directory \"{}\": {}",
                        ssh_key_dir.display(),
                        err
                    ),
                ));
            }
            debug!("Created SSH key directory");
        }
        // If Config file doesn't exist, create it
        if !config_path.exists() {
            if let Err(err) = client.write_config() {
                error!("Couldn't create configuration file: {}", err);
                return Err(err);
            }
            debug!("Config file didn't exist; created file");
        } else {
            // otherwise Load configuration from file
            if let Err(err) = client.read_config() {
                error!("Couldn't read configuration file: {}", err);
                return Err(err);
            }
            debug!("Read configuration file");
        }
        Ok(client)
    }

    /// Instantiate a ConfigClient in degraded mode.
    /// When in degraded mode, the configuration in use will be the default configuration
    /// and the IO operation on configuration won't be available
    pub fn degraded() -> Self {
        Self {
            config: UserConfig::default(),
            config_path: PathBuf::default(),
            ssh_key_dir: PathBuf::default(),
            degraded: true,
        }
    }

    // Text editor

    /// Get text editor from configuration
    pub fn get_text_editor(&self) -> PathBuf {
        self.config.user_interface.text_editor.clone()
    }

    /// Set text editor path
    pub fn set_text_editor(&mut self, path: PathBuf) {
        self.config.user_interface.text_editor = path;
    }

    // Default protocol

    /// Get default protocol from configuration
    pub fn get_default_protocol(&self) -> FileTransferProtocol {
        match FileTransferProtocol::from_str(self.config.user_interface.default_protocol.as_str()) {
            Ok(p) => p,
            Err(_) => FileTransferProtocol::Sftp,
        }
    }

    /// Set default protocol to configuration
    pub fn set_default_protocol(&mut self, proto: FileTransferProtocol) {
        self.config.user_interface.default_protocol = proto.to_string();
    }

    /// Get value of `show_hidden_files`
    pub fn get_show_hidden_files(&self) -> bool {
        self.config.user_interface.show_hidden_files
    }

    /// Set new value for `show_hidden_files`
    pub fn set_show_hidden_files(&mut self, value: bool) {
        self.config.user_interface.show_hidden_files = value;
    }

    /// Get value of `check_for_updates`
    pub fn get_check_for_updates(&self) -> bool {
        self.config.user_interface.check_for_updates.unwrap_or(true)
    }

    /// Set new value for `check_for_updates`
    pub fn set_check_for_updates(&mut self, value: bool) {
        self.config.user_interface.check_for_updates = Some(value);
    }

    /// Get value of `prompt_on_file_replace`
    pub fn get_prompt_on_file_replace(&self) -> bool {
        self.config
            .user_interface
            .prompt_on_file_replace
            .unwrap_or(true)
    }

    /// Set new value for `prompt_on_file_replace`
    pub fn set_prompt_on_file_replace(&mut self, value: bool) {
        self.config.user_interface.prompt_on_file_replace = Some(value);
    }

    /// Get GroupDirs value from configuration (will be converted from string)
    pub fn get_group_dirs(&self) -> Option<GroupDirs> {
        // Convert string to `GroupDirs`
        match &self.config.user_interface.group_dirs {
            None => None,
            Some(val) => GroupDirs::from_str(val.as_str()).ok(),
        }
    }

    /// Set value for group_dir in configuration.
    /// Provided value, if `Some` will be converted to `GroupDirs`
    pub fn set_group_dirs(&mut self, val: Option<GroupDirs>) {
        self.config.user_interface.group_dirs = val.map(|val| val.to_string());
    }

    /// Get current file fmt for local host
    pub fn get_local_file_fmt(&self) -> Option<String> {
        self.config.user_interface.file_fmt.clone()
    }

    /// Set file fmt parameter for local host
    pub fn set_local_file_fmt(&mut self, s: String) {
        self.config.user_interface.file_fmt = match s.is_empty() {
            true => None,
            false => Some(s),
        };
    }

    /// Get current file fmt for remote host
    pub fn get_remote_file_fmt(&self) -> Option<String> {
        self.config.user_interface.remote_file_fmt.clone()
    }

    /// Set file fmt parameter for remote host
    pub fn set_remote_file_fmt(&mut self, s: String) {
        self.config.user_interface.remote_file_fmt = match s.is_empty() {
            true => None,
            false => Some(s),
        };
    }

    /// Get value of `notifications`
    pub fn get_notifications(&self) -> bool {
        self.config.user_interface.notifications.unwrap_or(true)
    }

    /// Set new value for `notifications`
    pub fn set_notifications(&mut self, value: bool) {
        self.config.user_interface.notifications = Some(value);
    }

    /// Get value of `notification_threshold`
    pub fn get_notification_threshold(&self) -> u64 {
        self.config
            .user_interface
            .notification_threshold
            .unwrap_or(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD)
    }

    /// Set new value for `notification_threshold`
    pub fn set_notification_threshold(&mut self, value: u64) {
        self.config.user_interface.notification_threshold = Some(value);
    }

    // Remote params

    /// Get ssh config path
    pub fn get_ssh_config(&self) -> Option<&str> {
        self.config.remote.ssh_config.as_deref()
    }

    /// Set ssh config path
    pub fn set_ssh_config(&mut self, p: Option<String>) {
        self.config.remote.ssh_config = p;
    }

    // SSH Keys

    /// Save a SSH key into configuration.
    /// This operation also creates the key file in `ssh_key_dir`
    /// and also commits changes to configuration, to prevent incoerent data
    pub fn add_ssh_key(
        &mut self,
        host: &str,
        username: &str,
        ssh_key: &str,
    ) -> Result<(), SerializerError> {
        if self.degraded {
            return Err(SerializerError::new_ex(
                SerializerErrorKind::Generic,
                String::from("Configuration won't be saved, since in degraded mode"),
            ));
        }
        let host_name: String = Self::make_ssh_host_key(host, username);
        // Get key path
        let ssh_key_path: PathBuf = {
            let mut p: PathBuf = self.ssh_key_dir.clone();
            p.push(format!("{host_name}.key"));
            p
        };
        info!(
            "Writing SSH file to {} for host {}",
            ssh_key_path.display(),
            host_name
        );
        // Write key to file
        let mut f: File = match File::create(ssh_key_path.as_path()) {
            Ok(f) => f,
            Err(err) => return Self::make_io_err(err),
        };
        if let Err(err) = f.write_all(ssh_key.as_bytes()) {
            error!("Failed to write SSH key to file: {}", err);
            return Self::make_io_err(err);
        }
        // Add host to keys
        self.config.remote.ssh_keys.insert(host_name, ssh_key_path);
        // Write config
        self.write_config()
    }

    /// Delete a ssh key from configuration, using host as key.
    /// This operation also unlinks the key file in `ssh_key_dir`
    /// and also commits changes to configuration, to prevent incoerent data
    pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
        if self.degraded {
            return Err(SerializerError::new_ex(
                SerializerErrorKind::Generic,
                String::from("Configuration won't be saved, since in degraded mode"),
            ));
        }
        // Remove key from configuration and get key path
        info!("Removing key for {}@{}", host, username);
        let key_path: PathBuf = match self
            .config
            .remote
            .ssh_keys
            .remove(&Self::make_ssh_host_key(host, username))
        {
            Some(p) => p,
            None => return Ok(()), // Return ok if host doesn't exist
        };
        // Remove file
        if let Err(err) = remove_file(key_path.as_path()) {
            error!("Failed to remove key file {}: {}", key_path.display(), err);
            return Self::make_io_err(err);
        }
        // Commit changes to configuration
        self.write_config()
    }

    /// Get ssh key from host.
    /// None is returned if key doesn't exist
    pub fn get_ssh_key(&self, mkey: &str) -> Option<SshHost> {
        if self.degraded {
            return None;
        }
        // Check if Key exists
        match self.config.remote.ssh_keys.get(mkey) {
            None => None,
            Some(key_path) => {
                // Get host and username
                let (host, username) = Self::get_ssh_tokens(mkey)?;
                // Return key
                Some((host, username, PathBuf::from(key_path)))
            }
        }
    }

    /// Get an iterator through hosts in the ssh key storage
    pub fn iter_ssh_keys(&self) -> impl Iterator<Item = &String> + '_ {
        Box::new(self.config.remote.ssh_keys.keys())
    }

    // I/O

    /// Write configuration to file
    pub fn write_config(&self) -> Result<(), SerializerError> {
        if self.degraded {
            return Err(SerializerError::new_ex(
                SerializerErrorKind::Generic,
                String::from("Configuration won't be saved, since in degraded mode"),
            ));
        }
        // Open file
        match OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(self.config_path.as_path())
        {
            Ok(writer) => serialize(&self.config, Box::new(writer)),
            Err(err) => {
                error!("Failed to write configuration file: {}", err);
                Err(SerializerError::new_ex(
                    SerializerErrorKind::Io,
                    err.to_string(),
                ))
            }
        }
    }

    /// Read configuration from file (or reload it if already read)
    pub fn read_config(&mut self) -> Result<(), SerializerError> {
        if self.degraded {
            return Err(SerializerError::new_ex(
                SerializerErrorKind::Generic,
                String::from("Configuration won't be loaded, since in degraded mode"),
            ));
        }
        // Open bookmarks file for read
        match OpenOptions::new()
            .read(true)
            .open(self.config_path.as_path())
        {
            Ok(reader) => {
                // Deserialize
                match deserialize(Box::new(reader)) {
                    Ok(config) => {
                        self.config = config;
                        Ok(())
                    }
                    Err(err) => Err(err),
                }
            }
            Err(err) => {
                error!("Failed to read configuration: {}", err);
                Err(SerializerError::new_ex(
                    SerializerErrorKind::Io,
                    err.to_string(),
                ))
            }
        }
    }

    /// Hosts are saved as `username@host` into configuration.
    /// This method creates the key name, starting from host and username
    fn make_ssh_host_key(host: &str, username: &str) -> String {
        format!("{username}@{host}")
    }

    /// Get ssh tokens starting from ssh host key
    /// Returns: (host, username) or None if key has invalid syntax
    fn get_ssh_tokens(host_key: &str) -> Option<(String, String)> {
        let tokens: Vec<&str> = host_key.split('@').collect();
        if tokens.len() >= 2 {
            Some((String::from(tokens[1]), String::from(tokens[0])))
        } else {
            error!(
                "Invalid SSH host key format: '{}' (expected 'username@host')",
                host_key
            );
            None
        }
    }

    /// Make serializer error from `std::io::Error`
    fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> {
        Err(SerializerError::new_ex(
            SerializerErrorKind::Io,
            err.to_string(),
        ))
    }
}

#[cfg(test)]
mod tests {

    use std::io::Read;

    use pretty_assertions::assert_eq;
    use tempfile::TempDir;

    use super::*;
    use crate::config::params::UserConfig;
    use crate::utils::random::random_alphanumeric_with_len;

    #[test]
    fn test_system_config_new() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
            .ok()
            .unwrap();
        // Verify parameters
        let default_config: UserConfig = UserConfig::default();
        assert_eq!(client.degraded, false);
        assert_eq!(client.config.remote.ssh_keys.len(), 0);
        assert_eq!(
            client.config.user_interface.default_protocol,
            default_config.user_interface.default_protocol
        );
        assert_eq!(
            client.config.user_interface.text_editor,
            default_config.user_interface.text_editor
        );
        assert_eq!(client.config_path, cfg_path);
        assert_eq!(client.ssh_key_dir, ssh_keys_path);
    }

    #[test]
    fn test_system_config_degraded() {
        let mut client: ConfigClient = ConfigClient::degraded();
        assert_eq!(client.degraded, true);
        assert_eq!(client.config_path, PathBuf::default());
        assert_eq!(client.ssh_key_dir, PathBuf::default());
        // I/O
        assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
        assert!(client.del_ssh_key("omar", "omar").is_err());
        assert!(client.get_ssh_key("omar").is_none());
        assert!(client.write_config().is_err());
        assert!(client.read_config().is_err());
    }

    #[test]
    fn test_system_config_new_err() {
        assert!(
            ConfigClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar"),)
                .is_err()
        );
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        assert!(ConfigClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar")).is_err());
    }

    #[test]
    fn test_system_config_from_existing() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        // Change some stuff
        client.set_text_editor(PathBuf::from("/usr/bin/vim"));
        client.set_default_protocol(FileTransferProtocol::Scp);
        assert!(
            client
                .add_ssh_key("192.168.1.31", "pi", "piroporopero")
                .is_ok()
        );
        assert!(client.write_config().is_ok());
        // Istantiate a new client
        let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        // Verify client has updated parameters
        assert_eq!(client.get_default_protocol(), FileTransferProtocol::Scp);
        assert_eq!(client.get_text_editor(), PathBuf::from("/usr/bin/vim"));
        let mut expected_key_path: PathBuf = key_path;
        expected_key_path.push("pi@192.168.1.31.key");
        assert_eq!(
            client.get_ssh_key("pi@192.168.1.31").unwrap(),
            (
                String::from("192.168.1.31"),
                String::from("pi"),
                expected_key_path,
            )
        );
    }

    #[test]
    fn test_system_config_text_editor() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        client.set_text_editor(PathBuf::from("mcedit"));
        assert_eq!(client.get_text_editor(), PathBuf::from("mcedit"));
    }

    #[test]
    fn test_system_config_default_protocol() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        client.set_default_protocol(FileTransferProtocol::Ftp(true));
        assert_eq!(
            client.get_default_protocol(),
            FileTransferProtocol::Ftp(true)
        );
    }

    #[test]
    fn should_round_trip_gcs_as_default_protocol() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();

        client.set_default_protocol(FileTransferProtocol::GoogleCloudStorage);

        assert_eq!(
            client.get_default_protocol(),
            FileTransferProtocol::GoogleCloudStorage
        );
        assert_eq!(client.config.user_interface.default_protocol, "GCS");
    }

    #[test]
    fn test_system_config_show_hidden_files() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        client.set_show_hidden_files(true);
        assert_eq!(client.get_show_hidden_files(), true);
    }

    #[test]
    fn test_system_config_check_for_updates() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(client.get_check_for_updates(), true); // Null ?
        client.set_check_for_updates(true);
        assert_eq!(client.get_check_for_updates(), true);
        client.set_check_for_updates(false);
        assert_eq!(client.get_check_for_updates(), false);
    }

    #[test]
    fn test_system_config_prompt_on_file_replace() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(client.get_prompt_on_file_replace(), true); // Null ?
        client.set_prompt_on_file_replace(true);
        assert_eq!(client.get_prompt_on_file_replace(), true);
        client.set_prompt_on_file_replace(false);
        assert_eq!(client.get_prompt_on_file_replace(), false);
    }

    #[test]
    fn test_system_config_group_dirs() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        client.set_group_dirs(Some(GroupDirs::First));
        assert_eq!(client.get_group_dirs(), Some(GroupDirs::First),);
        client.set_group_dirs(None);
        assert_eq!(client.get_group_dirs(), None,);
    }

    #[test]
    fn test_system_config_local_file_fmt() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(client.get_local_file_fmt(), None);
        client.set_local_file_fmt(String::from("{NAME}"));
        assert_eq!(client.get_local_file_fmt().unwrap(), String::from("{NAME}"));
        // Delete
        client.set_local_file_fmt(String::new());
        assert_eq!(client.get_local_file_fmt(), None);
    }

    #[test]
    fn test_system_config_remote_file_fmt() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(client.get_remote_file_fmt(), None);
        client.set_remote_file_fmt(String::from("{NAME}"));
        assert_eq!(
            client.get_remote_file_fmt().unwrap(),
            String::from("{NAME}")
        );
        // Delete
        client.set_remote_file_fmt(String::new());
        assert_eq!(client.get_remote_file_fmt(), None);
    }

    #[test]
    fn test_system_config_notifications() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(client.get_notifications(), true); // Null ?
        client.set_notifications(true);
        assert_eq!(client.get_notifications(), true);
        client.set_notifications(false);
        assert_eq!(client.get_notifications(), false);
    }

    #[test]
    fn test_system_config_remote_notification_threshold() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        assert_eq!(
            client.get_notification_threshold(),
            DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD
        ); // Null ?
        client.set_notification_threshold(1024);
        assert_eq!(client.get_notification_threshold(), 1024);
        client.set_notification_threshold(64);
        assert_eq!(client.get_notification_threshold(), 64);
    }

    #[test]
    fn should_get_and_set_ssh_config_dir() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();

        let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/root"));
        let mut ssh_config_path = "~/.ssh/config".to_string();
        ssh_config_path = ssh_config_path.replacen('~', &home_dir.to_string_lossy(), 1);

        let ssh_config_path = if PathBuf::from(&ssh_config_path).exists() {
            Some(ssh_config_path)
        } else {
            None
        };

        assert_eq!(client.get_ssh_config(), ssh_config_path.as_deref()); // Null ?
        client.set_ssh_config(Some(String::from("/tmp/ssh_config")));
        assert_eq!(client.get_ssh_config(), Some("/tmp/ssh_config"));
        client.set_ssh_config(None);
        assert_eq!(client.get_ssh_config(), None);
    }

    #[test]
    fn test_system_config_ssh_keys() {
        let tmp_dir: TempDir = TempDir::new().ok().unwrap();
        let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
        let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
            .ok()
            .unwrap();
        // Add a new key
        let rsa_key: String = get_sample_rsa_key();
        assert!(
            client
                .add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
                .is_ok()
        );
        // Iterate keys
        for key in client.iter_ssh_keys() {
            let host: SshHost = client.get_ssh_key(key).unwrap();
            assert_eq!(host.0, String::from("192.168.1.31"));
            assert_eq!(host.1, String::from("pi"));
            let mut expected_key_path: PathBuf = key_path.clone();
            expected_key_path.push("pi@192.168.1.31.key");
            assert_eq!(host.2, expected_key_path);
            // Read rsa key
            let mut key_file: File = File::open(expected_key_path.as_path()).ok().unwrap();
            // Read
            let mut key: String = String::new();
            assert!(key_file.read_to_string(&mut key).is_ok());
            // Verify rsa key
            assert_eq!(key, rsa_key);
        }
        // Unexisting key
        assert!(client.get_ssh_key("test").is_none());
        // Delete key
        assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
    }

    #[test]
    fn test_system_config_make_key() {
        assert_eq!(
            ConfigClient::make_ssh_host_key("192.168.1.31", "pi"),
            String::from("pi@192.168.1.31")
        );
        assert_eq!(
            ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
            Some((String::from("192.168.1.31"), String::from("pi")))
        );
    }

    #[test]
    fn test_system_config_get_ssh_tokens_invalid() {
        assert!(ConfigClient::get_ssh_tokens("invalid").is_none());
        assert!(ConfigClient::get_ssh_tokens("").is_none());
    }

    #[test]
    fn test_system_config_make_io_err() {
        let err: SerializerError =
            ConfigClient::make_io_err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
                .err()
                .unwrap();
        assert_eq!(err.to_string(), "IO error (permission denied)");
    }

    /// Get paths for configuration and keys directory
    fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
        let mut k: PathBuf = PathBuf::from(dir);
        let mut c: PathBuf = k.clone();
        k.push("ssh-keys/");
        c.push("config.toml");
        (c, k)
    }

    fn get_sample_rsa_key() -> String {
        format!(
            "-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",
            random_alphanumeric_with_len(2536)
        )
    }
}
