// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 Keylime Authors
use crate::{
    config::{file_config, EnvConfig, KeylimeConfigError},
    hostname_parser::parse_hostname,
    ip_parser::parse_ip,
    list_parser::parse_list,
    permissions,
    version::{self, GetErrorInput},
};
use config::{
    builder::DefaultState, Config, ConfigBuilder, ConfigError, Map, Source,
    Value,
};
use log::*;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::{env, path::Path, str::FromStr};
use uuid::Uuid;

pub static CONFIG_VERSION: &str = "2.0";
pub static SUPPORTED_API_VERSIONS: &[&str] = &["2.1", "2.2"];
pub static DEFAULT_REGISTRAR_API_VERSIONS: &[&str] = &["2.3"];

pub static DEFAULT_CONFIG: &str = "/etc/keylime/agent.conf";
pub static DEFAULT_CONFIG_SNIPPETS_DIR: &str = "/etc/keylime/agent.conf.d";
pub static DEFAULT_SYS_CONFIG: &str = "/usr/etc/keylime/agent.conf";
pub static DEFAULT_SYS_CONFIG_SNIPPETS_DIR: &str =
    "/usr/etc/keylime/agent.conf.d";

pub static DEFAULT_AGENT_DATA_PATH: &str = "agent_data.json";
pub static DEFAULT_API_VERSIONS: &str = "default";
pub static DEFAULT_UUID: &str = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000";
pub static DEFAULT_IP: &str = "127.0.0.1";
pub static DEFAULT_PORT: u32 = 9002;
pub static DEFAULT_REGISTRAR_IP: &str = "127.0.0.1";
pub static DEFAULT_REGISTRAR_PORT: u32 = 8890;
pub static DEFAULT_KEYLIME_DIR: &str = "/var/lib/keylime";
pub static DEFAULT_IAK_CERT: &str = "iak-cert.crt";
pub static DEFAULT_IDEVID_CERT: &str = "idevid-cert.crt";
pub static DEFAULT_TPM_OWNERPASSWORD: &str = "";
// Note: The revocation certificate name is generated inside the Python tenant and the
// certificate(s) can be generated by running the tenant with the --cert flag. For more
// information, check the README: https://github.com/keylime/keylime/#using-keylime-ca
pub static DEFAULT_REVOCATION_CERT: &str = "RevocationNotifier-cert.crt";
pub static DEFAULT_EK_HANDLE: &str = "generate";
pub static DEFAULT_ENABLE_IAK_IDEVID: bool = false;
pub static DEFAULT_IAK_IDEVID_ASYMMETRIC_ALG: &str = "rsa";
pub static DEFAULT_IAK_IDEVID_NAME_ALG: &str = "sha256";
pub static DEFAULT_IAK_IDEVID_TEMPLATE: &str = "H-1";
pub static DEFAULT_IDEVID_PASSWORD: &str = "";
pub static DEFAULT_IAK_PASSWORD: &str = "";
pub static DEFAULT_IDEVID_HANDLE: &str = "";
pub static DEFAULT_IAK_HANDLE: &str = "";
pub static DEFAULT_RUN_AS: &str = "keylime:tss";
pub static DEFAULT_IMA_ML_PATH: &str =
    "/sys/kernel/security/ima/ascii_runtime_measurements";
pub static DEFAULT_TPM_ENCRYPTION_ALG: &str = "rsa";
pub static DEFAULT_TPM_HASH_ALG: &str = "sha256";
pub static DEFAULT_TPM_SIGNING_ALG: &str = "rsassa";

// Pull attestation agent options defaults
pub static DEFAULT_ALLOW_PAYLOAD_REVOCATION_ACTIONS: bool = true;
pub static DEFAULT_CONTACT_IP: &str = "127.0.0.1";
pub static DEFAULT_CONTACT_PORT: u32 = 9002;
pub static DEFAULT_DEC_PAYLOAD_FILE: &str = "decrypted_payload";
pub static DEFAULT_ENABLE_AGENT_MTLS: bool = true;
pub static DEFAULT_ENABLE_INSECURE_PAYLOAD: bool = false;
pub static DEFAULT_ENABLE_REVOCATION_NOTIFICATIONS: bool = false;
pub static DEFAULT_ENC_KEYNAME: &str = "derived_tci_key";
pub static DEFAULT_EXTRACT_PAYLOAD_ZIP: bool = true;
pub static DEFAULT_MEASUREDBOOT_ML_PATH: &str =
    "/sys/kernel/security/tpm0/binary_bios_measurements";
pub static DEFAULT_PAYLOAD_SCRIPT: &str = "autorun.sh";
pub static DEFAULT_REVOCATION_ACTIONS: &str = "";
pub static DEFAULT_REVOCATION_ACTIONS_DIR: &str = "/usr/libexec/keylime";
pub static DEFAULT_REVOCATION_NOTIFICATION_IP: &str = "127.0.0.1";
pub static DEFAULT_REVOCATION_NOTIFICATION_PORT: u32 = 8992;
pub static DEFAULT_SECURE_SIZE: &str = "1m";
pub static DEFAULT_SERVER_CERT: &str = "server-cert.crt";
pub static DEFAULT_SERVER_KEY: &str = "server-private.pem";
pub static DEFAULT_SERVER_KEY_PASSWORD: &str = "";
// The DEFAULT_TRUSTED_CLIENT_CA is relative from KEYLIME_DIR
pub static DEFAULT_TRUSTED_CLIENT_CA: &str = "cv_ca/cacert.crt";

// Push attestation agent option defaults
pub const DEFAULT_DISABLED_SIGNING_ALGORITHMS: &[&str] = &["ecschnorr"];
pub const DEFAULT_IMA_LOGS_APPENDABLE: bool = true;
pub const DEFAULT_IMA_LOGS_FORMATS: &str = "text/plain";
pub const DEFAULT_IMA_LOGS_SUPPORTS_PARTIAL_ACCESS: bool = true;
pub const DEFAULT_IMA_ML_DIRECTORY_PATH: &str = "/sys/kernel/security/ima";
pub const DEFAULT_IMA_ML_COUNT_FILE: &str =
    "/sys/kernel/security/ima/measurements";
pub const DEFAULT_UEFI_LOGS_APPENDABLE: bool = true;
pub const DEFAULT_UEFI_LOGS_EVIDENCE_VERSION: &str = "2.1";
pub const DEFAULT_UEFI_LOGS_FORMATS: &str = "application/octet-stream";
pub const DEFAULT_UEFI_LOGS_SUPPORTS_PARTIAL_ACCESS: bool = true;
pub const DEFAULT_UEFI_LOGS_BINARY_FILE_PATH: &str =
    "/sys/kernel/security/tpm0/binary_bios_measurements";

// TODO These should be temporary
pub const DEFAULT_CERTIFICATION_KEYS_SERVER_IDENTIFIER: &str = "ak";
pub static DEFAULT_PUSH_API_VERSIONS: &[&str] = &["3.0"];
pub static DEFAULT_PUSH_EK_HANDLE: &str = "";

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct AgentConfig {
    pub agent_data_path: String,
    pub api_versions: String,
    pub disabled_signing_algorithms: Vec<String>,
    pub ek_handle: String,
    pub enable_iak_idevid: bool,
    pub iak_cert: String,
    pub iak_handle: String,
    pub iak_idevid_asymmetric_alg: String,
    pub iak_idevid_name_alg: String,
    pub iak_idevid_template: String,
    pub iak_password: String,
    pub idevid_cert: String,
    pub idevid_handle: String,
    pub idevid_password: String,
    pub ima_ml_path: String,
    pub ip: String,
    pub keylime_dir: String,
    pub port: u32,
    pub registrar_ip: String,
    pub registrar_port: u32,
    pub run_as: String,
    pub tpm_encryption_alg: String,
    pub tpm_hash_alg: String,
    pub tpm_ownerpassword: String,
    pub tpm_signing_alg: String,
    pub trusted_client_ca: String,
    pub uuid: String,
    pub version: String,

    // Pull attestation options
    pub allow_payload_revocation_actions: bool,
    pub contact_ip: String,
    pub contact_port: u32,
    pub dec_payload_file: String,
    pub enable_agent_mtls: bool,
    pub enable_insecure_payload: bool,
    pub enable_revocation_notifications: bool,
    pub enc_keyname: String,
    pub extract_payload_zip: bool,
    pub measuredboot_ml_path: String,
    pub payload_script: String,
    pub revocation_actions: String,
    pub revocation_actions_dir: String,
    pub revocation_cert: String,
    pub revocation_notification_ip: String,
    pub revocation_notification_port: u32,
    pub secure_size: String,
    pub server_cert: String,
    pub server_key: String,
    pub server_key_password: String,

    // Push attestation options
    pub certification_keys_server_identifier: String,
    pub ima_logs_appendable: bool,
    pub ima_logs_formats: String,
    pub ima_logs_supports_partial_access: bool,
    pub ima_ml_count_file: String,
    pub registrar_api_versions: String,
    pub uefi_logs_binary_file_path: String,
    pub uefi_logs_evidence_version: String,
    pub uefi_logs_supports_partial_access: bool,
    pub uefi_logs_appendable: bool,
    pub uefi_logs_formats: String,
}

impl AgentConfig {
    pub fn new() -> Result<Self, KeylimeConfigError> {
        #[derive(Deserialize)]
        struct Wrapper {
            agent: AgentConfig,
        }
        // Get the base configuration file from the environment variable or the default locations
        let config: Wrapper =
            config_get_setting()?.build()?.try_deserialize()?;

        // Replace keywords with actual values
        config_translate_keywords(&config.agent)
    }
}

// Helper function to convert serde_json::Value to config::Value
fn json_to_config_value(json_value: JsonValue) -> Result<Value, ConfigError> {
    match json_value {
        JsonValue::Bool(b) => Ok(Value::from(b)),
        JsonValue::Number(n) => {
            if let Some(i) = n.as_i64() {
                Ok(Value::from(i))
            } else if let Some(u) = n.as_u64() {
                Ok(Value::from(u))
            } else if let Some(f) = n.as_f64() {
                Ok(Value::from(f))
            } else {
                Err(ConfigError::Foreign(Box::new(
                    KeylimeConfigError::JsonConversion,
                )))
            }
        }
        JsonValue::String(s) => Ok(Value::from(s)),
        JsonValue::Array(arr) => Ok(Value::from(
            arr.into_iter()
                .map(json_to_config_value)
                .collect::<Result<Vec<_>, ConfigError>>()?,
        )),
        JsonValue::Object(obj) => {
            let mut m = Map::new();
            for (k, v) in obj.into_iter() {
                _ = m.insert(k, json_to_config_value(v)?)
            }
            Ok(Value::from(m))
        }
        JsonValue::Null => Err(ConfigError::Foreign(Box::new(
            KeylimeConfigError::JsonConversion,
        ))),
    }
}

impl Source for AgentConfig {
    fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
        let json = serde_json::to_value(self)
            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

        let config_map = json_to_config_value(json)?.into_table()?;

        Ok(Map::from([("agent".to_string(), Value::from(config_map))]))
    }

    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
        Box::new(self.clone())
    }
}

impl Default for AgentConfig {
    fn default() -> Self {
        // In case the process is executed by privileged user
        let run_as = if permissions::get_euid() == 0 {
            DEFAULT_RUN_AS.to_string()
        } else {
            "".to_string()
        };

        AgentConfig {
            agent_data_path: "default".to_string(),
            allow_payload_revocation_actions:
                DEFAULT_ALLOW_PAYLOAD_REVOCATION_ACTIONS,
            api_versions: DEFAULT_API_VERSIONS.to_string(),
            contact_ip: DEFAULT_CONTACT_IP.to_string(),
            contact_port: DEFAULT_CONTACT_PORT,
            dec_payload_file: DEFAULT_DEC_PAYLOAD_FILE.to_string(),
            disabled_signing_algorithms: DEFAULT_DISABLED_SIGNING_ALGORITHMS
                .iter()
                .map(|s| s.to_string())
                .collect(),
            ek_handle: DEFAULT_EK_HANDLE.to_string(),
            enable_agent_mtls: DEFAULT_ENABLE_AGENT_MTLS,
            enable_iak_idevid: DEFAULT_ENABLE_IAK_IDEVID,
            enable_insecure_payload: DEFAULT_ENABLE_INSECURE_PAYLOAD,
            enable_revocation_notifications:
                DEFAULT_ENABLE_REVOCATION_NOTIFICATIONS,
            enc_keyname: DEFAULT_ENC_KEYNAME.to_string(),
            extract_payload_zip: DEFAULT_EXTRACT_PAYLOAD_ZIP,
            iak_cert: "default".to_string(),
            iak_handle: DEFAULT_IAK_HANDLE.to_string(),
            iak_idevid_asymmetric_alg: DEFAULT_IAK_IDEVID_ASYMMETRIC_ALG
                .to_string(),
            iak_idevid_name_alg: DEFAULT_IAK_IDEVID_NAME_ALG.to_string(),
            iak_idevid_template: DEFAULT_IAK_IDEVID_TEMPLATE.to_string(),
            iak_password: DEFAULT_IAK_PASSWORD.to_string(),
            idevid_cert: "default".to_string(),
            idevid_handle: DEFAULT_IDEVID_HANDLE.to_string(),
            idevid_password: DEFAULT_IDEVID_PASSWORD.to_string(),
            ima_ml_path: "default".to_string(),
            ip: DEFAULT_IP.to_string(),
            keylime_dir: DEFAULT_KEYLIME_DIR.to_string(),
            measuredboot_ml_path: "default".to_string(),
            payload_script: DEFAULT_PAYLOAD_SCRIPT.to_string(),
            port: DEFAULT_PORT,
            registrar_ip: DEFAULT_REGISTRAR_IP.to_string(),
            registrar_port: DEFAULT_REGISTRAR_PORT,
            revocation_actions: DEFAULT_REVOCATION_ACTIONS.to_string(),
            revocation_actions_dir: DEFAULT_REVOCATION_ACTIONS_DIR
                .to_string(),
            revocation_cert: "default".to_string(),
            revocation_notification_ip: DEFAULT_REVOCATION_NOTIFICATION_IP
                .to_string(),
            revocation_notification_port:
                DEFAULT_REVOCATION_NOTIFICATION_PORT,
            run_as,
            secure_size: DEFAULT_SECURE_SIZE.to_string(),
            server_cert: "default".to_string(),
            server_key: "default".to_string(),
            server_key_password: DEFAULT_SERVER_KEY_PASSWORD.to_string(),
            tpm_encryption_alg: DEFAULT_TPM_ENCRYPTION_ALG.to_string(),
            tpm_hash_alg: DEFAULT_TPM_HASH_ALG.to_string(),
            tpm_ownerpassword: DEFAULT_TPM_OWNERPASSWORD.to_string(),
            tpm_signing_alg: DEFAULT_TPM_SIGNING_ALG.to_string(),
            trusted_client_ca: "default".to_string(),
            uuid: DEFAULT_UUID.to_string(),
            version: CONFIG_VERSION.to_string(),
            certification_keys_server_identifier:
                DEFAULT_CERTIFICATION_KEYS_SERVER_IDENTIFIER.to_string(),
            ima_logs_appendable: DEFAULT_IMA_LOGS_APPENDABLE,
            ima_logs_formats: DEFAULT_IMA_LOGS_FORMATS.to_string(),
            ima_logs_supports_partial_access:
                DEFAULT_IMA_LOGS_SUPPORTS_PARTIAL_ACCESS,
            ima_ml_count_file: DEFAULT_IMA_ML_COUNT_FILE.to_string(),
            registrar_api_versions: DEFAULT_REGISTRAR_API_VERSIONS.join(", "),
            uefi_logs_binary_file_path: DEFAULT_UEFI_LOGS_BINARY_FILE_PATH
                .to_string(),
            uefi_logs_evidence_version: DEFAULT_UEFI_LOGS_EVIDENCE_VERSION
                .to_string(),
            uefi_logs_supports_partial_access:
                DEFAULT_UEFI_LOGS_SUPPORTS_PARTIAL_ACCESS,
            uefi_logs_appendable: DEFAULT_UEFI_LOGS_APPENDABLE,
            uefi_logs_formats: DEFAULT_UEFI_LOGS_FORMATS.to_string(),
        }
    }
}

fn config_get_setting(
) -> Result<ConfigBuilder<DefaultState>, KeylimeConfigError> {
    // Load the configuration from the default file locations
    let default_config = file_config::load_default_files()?;

    // Load the configuration overrides from the environment
    let env_config = EnvConfig::new()?;

    let builder = Config::builder()
        // Default values
        .add_source(default_config)
        // Add environment variables overrides
        .add_source(env_config);

    Ok(builder)
}

/// Replace the options that support keywords with the final value
fn config_translate_keywords(
    config: &AgentConfig,
) -> Result<AgentConfig, KeylimeConfigError> {
    let uuid = get_uuid(&config.uuid);

    let env_keylime_dir = env::var("KEYLIME_DIR").ok();

    let keylime_dir = if let Some(ref dir) = env_keylime_dir {
        if !dir.is_empty() {
            Path::new(dir)
        } else if config.keylime_dir.is_empty() {
            Path::new(DEFAULT_KEYLIME_DIR)
        } else {
            Path::new(&config.keylime_dir)
        }
    } else if config.keylime_dir.is_empty() {
        Path::new(DEFAULT_KEYLIME_DIR)
    } else {
        Path::new(&config.keylime_dir)
    };

    // Validate that keylime_dir exists
    #[cfg(not(test))]
    let keylime_dir = &keylime_dir.canonicalize().map_err(|e| {
        KeylimeConfigError::MissingKeylimeDir {
            path: keylime_dir.display().to_string(),
            source: e,
        }
    })?;

    let root_path = Path::new("/");

    let agent_data_path = config_get_file_path(
        "agent_data_path",
        &config.agent_data_path,
        keylime_dir,
        DEFAULT_AGENT_DATA_PATH,
        false,
    );

    let ima_ml_path = config_get_file_path(
        "ima_ml_path",
        &config.ima_ml_path,
        root_path,
        DEFAULT_IMA_ML_PATH,
        false,
    );

    let measuredboot_ml_path = config_get_file_path(
        "measuredboot_ml_path",
        &config.measuredboot_ml_path,
        root_path,
        DEFAULT_MEASUREDBOOT_ML_PATH,
        false,
    );

    let server_key = config_get_file_path(
        "server_key",
        &config.server_key,
        keylime_dir,
        DEFAULT_SERVER_KEY,
        false,
    );

    let server_cert = config_get_file_path(
        "server_cert",
        &config.server_cert,
        keylime_dir,
        DEFAULT_SERVER_CERT,
        false,
    );

    let trusted_client_ca: String = parse_list(&config.trusted_client_ca)?
        .iter()
        .map(|t| {
            config_get_file_path(
                "trusted_client_ca",
                t,
                keylime_dir,
                DEFAULT_TRUSTED_CLIENT_CA,
                false,
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    let iak_cert = config_get_file_path(
        "iak_cert",
        &config.iak_cert,
        keylime_dir,
        DEFAULT_IAK_CERT,
        true,
    );

    let idevid_cert = config_get_file_path(
        "idevid_cert",
        &config.idevid_cert,
        keylime_dir,
        DEFAULT_IDEVID_CERT,
        true,
    );

    let ek_handle = match config.ek_handle.as_ref() {
        "generate" => "".to_string(),
        "" => "".to_string(),
        s => s.to_string(),
    };

    let ip = match parse_ip(config.ip.as_ref()) {
        Ok(ip) => ip.to_string(),
        Err(_) => {
            debug!("Parsing configured IP as hostname");
            parse_hostname(config.ip.as_ref())?.to_string()
        }
    };

    let contact_ip = match parse_ip(config.contact_ip.as_ref()) {
        Ok(ip) => ip.to_string(),
        Err(_) => {
            debug!("Parsing configured contact IP as hostname");
            parse_hostname(config.contact_ip.as_ref())?.to_string()
        }
    };

    let registrar_ip = match parse_ip(config.registrar_ip.as_ref()) {
        Ok(ip) => ip.to_string(),
        Err(_) => {
            debug!("Parsing configured registrar IP as hostname");
            parse_hostname(config.registrar_ip.as_ref())?.to_string()
        }
    };

    // Parse the configured API versions and check against the list of supported versions
    // In case none of the configured API versions are supported, fallback to use all the supported
    // versions
    // If the "default" keyword is used, use all the supported versions
    // If the "latest" keyword is used, use only the latest version
    let api_versions: String = match config.api_versions.as_ref() {
        "default" => SUPPORTED_API_VERSIONS
            .iter()
            .map(|&s| s.to_string())
            .collect::<Vec<String>>()
            .join(", "),
        "latest" => {
            if let Some(version) = SUPPORTED_API_VERSIONS
                .iter()
                .map(|&s| s.to_string())
                .next_back()
            {
                version
            } else {
                unreachable!();
            }
        }
        versions => {
            let parsed: Vec<String> = match parse_list(versions) {
                Ok(list) => {
                    let mut filtered_versions = list
                    .iter()
                    .inspect(|e| { if !SUPPORTED_API_VERSIONS.contains(e) {
                        warn!("Skipping API version \"{e}\" obtained from 'api_versions' configuration option")
                    }})
                    .filter(|e| SUPPORTED_API_VERSIONS.contains(e))
                    .map(|&s| version::Version::from_str(s))
                    .inspect(|err| if let Err(e) = err {
                        warn!("Skipping API version \"{}\" obtained from 'api_versions' configuration option", e.input());
                    })
                    .filter(|e| e.is_ok())
                    .map(|v| {
                        let Ok(ver) = v else {unreachable!();};
                        ver
                    })
                    .collect::<Vec<version::Version>>();

                    // Sort the versions from the configuration from the oldest to the newest
                    filtered_versions.sort();
                    filtered_versions
                        .iter()
                        .map(|v| v.to_string())
                        .collect::<Vec<String>>()
                }
                Err(_) => {
                    warn!("Failed to parse list from 'api_versions' configuration option; using default supported versions");
                    SUPPORTED_API_VERSIONS.iter().map(|&s| s.into()).collect()
                }
            };

            if parsed.is_empty() {
                warn!("No supported version found in 'api_versions' configuration option; using default supported versions");
                SUPPORTED_API_VERSIONS
                    .iter()
                    .map(|&s| s.to_string())
                    .collect::<Vec<String>>()
                    .join(", ")
            } else {
                parsed.join(", ")
            }
        }
    };

    // Validate the configuration

    // If revocation notifications is enabled, verify all the required options for revocation
    if config.enable_revocation_notifications {
        if config.revocation_notification_ip.is_empty() {
            error!("The option 'enable_revocation_notifications' is set as 'true' but 'revocation_notification_ip' was set as empty");
            return Err(KeylimeConfigError::IncompatibleOptions {
                option_a: "enable_revocation_notifications".into(),
                value_a: "true".into(),
                option_b: "revocation_notification_ip".into(),
                value_b: "empty".into(),
            });
        }
        if config.revocation_cert.is_empty() {
            error!("The option 'enable_revocation_notifications' is set as 'true' 'revocation_cert' was set as empty");
            return Err(KeylimeConfigError::IncompatibleOptions {
                option_a: "enable_revocation_notifications".into(),
                value_a: "true".into(),
                option_b: "revocation_notification_cert".into(),
                value_b: "empty".into(),
            });
        }

        let actions_dir = match config.revocation_actions_dir.as_ref() {
            "" => {
                error!("The option 'enable_revocation_notifications' is set as 'true' but the revocation actions directory was set as empty in 'revocation_actions_dir'");
                return Err(KeylimeConfigError::IncompatibleOptions {
                    option_a: "enable_revocation_notifications".into(),
                    value_a: "true".into(),
                    option_b: "revocation_actions_dir".into(),
                    value_b: "empty".into(),
                });
            }
            dir => Path::new(dir),
        };

        // Validate that the revocation actions directory exists
        let _revocation_actions_dir =
            &actions_dir.canonicalize().map_err(|e| {
                KeylimeConfigError::MissingActionsDir {
                    path: keylime_dir.display().to_string(),
                    source: e,
                }
            })?;
    }

    let revocation_cert = config_get_file_path(
        "revocation_cert",
        &config.revocation_cert,
        keylime_dir,
        &format!("secure/unzipped/{DEFAULT_REVOCATION_CERT}"),
        false,
    );

    Ok(AgentConfig {
        agent_data_path,
        api_versions,
        contact_ip,
        ek_handle,
        iak_cert,
        idevid_cert,
        ima_ml_path,
        ip,
        keylime_dir: keylime_dir.display().to_string(),
        measuredboot_ml_path,
        registrar_ip,
        revocation_cert,
        server_cert,
        server_key,
        trusted_client_ca,
        uuid,
        ..config.clone()
    })
}

/// Expand a file path from the configuration file.
///
/// If the string is set as "default", return the provided default path relative from the provided work_dir.
/// If the string is empty, use the default value unless the 'leave_empty' is 'true'
/// If the string is a relative path, return the path relative from the provided work_dir
/// If the string is an absolute path, return the path without change.
fn config_get_file_path(
    option: &str,
    path: &str,
    work_dir: &Path,
    default: &str,
    leave_empty: bool,
) -> String {
    match path {
        "default" => work_dir.join(default).display().to_string(),
        "" => {
            if leave_empty {
                return "".to_string();
            }

            warn!("Empty string provided in configuration option {option}, using default {default}");
            work_dir.join(default).display().to_string()
        }
        v => {
            let p = Path::new(v);
            if p.is_relative() {
                work_dir.join(p).display().to_string()
            } else {
                p.display().to_string()
            }
        }
    }
}

fn get_uuid(agent_uuid_config: &str) -> String {
    match agent_uuid_config {
        "hash_ek" => {
            info!("Using hashed EK as UUID");
            // DO NOT change this to something else. It is used later to set the correct value.
            "hash_ek".into()
        }
        "generate" => {
            let agent_uuid = Uuid::new_v4();
            info!("Generated a new UUID: {}", &agent_uuid);
            agent_uuid.to_string()
        }
        uuid_config => match Uuid::parse_str(uuid_config) {
            Ok(uuid_config) => uuid_config.to_string(),
            Err(_) => {
                warn!("Misformatted UUID: {}", &uuid_config);
                let agent_uuid = Uuid::new_v4();
                info!("Using generated UUID: {}", &agent_uuid);
                agent_uuid.to_string()
            }
        },
    }
}

/// Create a configuration based on a temporary directory
///
/// # Arguments
///
/// `tempdir`: Path to be used as the keylime directory in the generated configuration
///
/// # Returns
///
/// A `AgentConfig` structure using the given path as the `keylime_dir` option
#[cfg(feature = "testing")]
pub fn get_testing_config(tempdir: &Path) -> AgentConfig {
    let config = AgentConfig {
        keylime_dir: tempdir.display().to_string(),
        ..AgentConfig::default()
    };

    // It is expected that the translation of keywords will not fail
    config_translate_keywords(&config).expect("failed to translate keywords")
}

// Unit Testing
#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(feature = "testing")]
    #[test]
    fn test_get_testing_config() {
        let dir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        // Get the config and check that the value is correct
        let config = get_testing_config(dir.path());
        assert_eq!(config.keylime_dir, dir.path().display().to_string());
    }

    #[cfg(feature = "testing")]
    #[test]
    fn test_default() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let default = get_testing_config(tempdir.path());

        // Modify the keylime directory to refer to the created temporary directory
        let c = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            ..AgentConfig::default()
        };

        let result = config_translate_keywords(&c);
        assert!(result.is_ok());
        let expected = result.unwrap(); //#[allow_ci]
        assert_eq!(expected, default);
    }

    #[test]
    fn test_hostname_support() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let default = AgentConfig::default();

        let modified = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            ip: "localhost".to_string(),
            contact_ip: "contact.ip".to_string(),
            registrar_ip: "registrar.ip".to_string(),
            ..default
        };

        let result = config_translate_keywords(&modified);
        assert!(result.is_ok());
        let result = result.unwrap(); //#[allow_ci]
        let resulting_ip = result.ip;
        let resulting_contact_ip = result.contact_ip;
        let resulting_registrar_ip = result.registrar_ip;
        assert_eq!(resulting_ip, "localhost");
        assert_eq!(resulting_contact_ip, "contact.ip");
        assert_eq!(resulting_registrar_ip, "registrar.ip");
    }

    #[cfg(feature = "testing")]
    #[test]
    fn get_revocation_cert_path_default() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let test_config = get_testing_config(tempdir.path());
        let revocation_cert_path = test_config.revocation_cert.clone();
        let expected = Path::new(&test_config.keylime_dir)
            .join("secure/unzipped")
            .join(DEFAULT_REVOCATION_CERT)
            .display()
            .to_string();
        assert_eq!(revocation_cert_path, expected);
    }

    #[test]
    fn get_revocation_cert_path_absolute() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            revocation_cert: "/test/cert.crt".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let test_config = result.unwrap(); //#[allow_ci]
        let revocation_cert_path = test_config.revocation_cert;
        let expected = Path::new("/test/cert.crt").display().to_string();
        assert_eq!(revocation_cert_path, expected);
    }

    #[test]
    fn get_revocation_cert_path_relative() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            revocation_cert: "cert.crt".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let test_config = result.unwrap(); //#[allow_ci]
        let revocation_cert_path = test_config.revocation_cert.clone();
        let expected = Path::new(&test_config.keylime_dir)
            .join("cert.crt")
            .display()
            .to_string();
        assert_eq!(revocation_cert_path, expected);
    }

    #[test]
    fn get_revocation_notification_ip_empty() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: true,
            revocation_notification_ip: "".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        // Due to enable_revocation_notifications being set
        assert!(result.is_err());
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: false,
            revocation_notification_ip: "".to_string(),
            ..Default::default()
        };

        // Now unset enable_revocation_notifications and check that is allowed
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let test_config = result.unwrap(); //#[allow_ci]
        assert_eq!(test_config.revocation_notification_ip, "".to_string());
    }

    #[test]
    fn get_revocation_cert_empty() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: true,
            revocation_cert: "".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        // Due to enable_revocation_notifications being set
        assert!(result.is_err());
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: false,
            revocation_cert: "".to_string(),
            ..Default::default()
        };

        // Now unset enable_revocation_notifications and check that is allowed
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
    }

    #[test]
    fn get_revocation_actions_dir_empty() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: true,
            revocation_actions_dir: "".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        // Due to enable_revocation_notifications being set
        assert!(result.is_err());
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: false,
            revocation_actions_dir: "".to_string(),
            ..Default::default()
        };

        // Now unset enable_revocation_notifications and check that is allowed
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_invalid_revocation_actions_dir() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: true,
            revocation_actions_dir: "/invalid".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        // Expect error due to the inexistent directory
        assert!(result.is_err());
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            enable_revocation_notifications: false,
            revocation_actions_dir: "/invalid".to_string(),
            ..Default::default()
        };

        // Now unset enable_revocation_notifications and check that is allowed
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_keylime_dir_option() {
        let dir = tempfile::tempdir()
            .expect("Failed to create temporary directory");
        let test_config = AgentConfig {
            keylime_dir: dir.path().display().to_string(),
            ..Default::default()
        };

        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_invalid_api_versions() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        // Check that invalid API versions are ignored
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: "invalid.api".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());

        // Check that unsupported API versions are ignored
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: "['0.0']".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());

        // Check that 'latest' keyword is supported
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: "\"latest\"".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_translate_api_versions_latest_keyword() {
        let tempdir = tempfile::tempdir()
            .expect("failed to create temporary directory");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: "latest".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        let expected = SUPPORTED_API_VERSIONS
            .iter()
            .map(|e| e.to_string())
            .next_back()
            .unwrap(); //#[allow_ci]
        assert_eq!(version, expected);
    }

    #[test]
    fn test_translate_api_versions_default_keyword() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        let expected = SUPPORTED_API_VERSIONS
            .iter()
            .map(|e| e.to_string())
            .collect::<Vec<_>>()
            .join(", ");
        assert_eq!(version, expected);
    }

    #[test]
    fn test_translate_api_versions_old_supported() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let old = SUPPORTED_API_VERSIONS[0];

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: old.to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        assert_eq!(version, old);
    }

    #[test]
    fn test_translate_invalid_api_versions_filtered() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let old = SUPPORTED_API_VERSIONS[0];

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: format!("a.b, {old}, c.d"),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        assert_eq!(version, old);
    }

    #[test]
    fn test_translate_invalid_api_versions_fallback_default() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let old = SUPPORTED_API_VERSIONS;

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: "a.b, c.d".to_string(),
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        assert_eq!(version, old.join(", "));
    }

    #[test]
    fn test_translate_api_versions_sort() {
        let tempdir =
            tempfile::tempdir().expect("failed to create temporary dir");
        let old = SUPPORTED_API_VERSIONS;
        let reversed = SUPPORTED_API_VERSIONS
            .iter()
            .rev()
            .copied()
            .collect::<Vec<_>>()
            .join(", ");

        let test_config = AgentConfig {
            keylime_dir: tempdir.path().display().to_string(),
            api_versions: reversed,
            ..Default::default()
        };
        let result = config_translate_keywords(&test_config);
        assert!(result.is_ok());
        let config = result.unwrap(); //#[allow_ci]
        let version = config.api_versions;
        assert_eq!(version, old.join(", "));
    }

    #[test]
    fn test_get_uuid() {
        assert_eq!(get_uuid("hash_ek"), "hash_ek");
        let _ = Uuid::parse_str(&get_uuid("generate")).unwrap(); //#[allow_ci]
        assert_eq!(
            get_uuid("D432FBB3-D2F1-4A97-9EF7-75BD81C00000"),
            "d432fbb3-d2f1-4a97-9ef7-75bd81c00000"
        );
        assert_ne!(
            get_uuid("D432FBB3-D2F1-4A97-9EF7-75BD81C0000X"),
            "d432fbb3-d2f1-4a97-9ef7-75bd81c0000X"
        );
        let _ = Uuid::parse_str(&get_uuid(
            "D432FBB3-D2F1-4A97-9EF7-75BD81C0000X",
        ))
        .unwrap(); //#[allow_ci]
    }

    #[test]
    fn test_agent_config_as_source() {
        #[derive(Deserialize, Serialize)]
        struct Wrapper {
            agent: AgentConfig,
        }

        let default = AgentConfig::default();

        // Test that the AgentConfig can be used as a source for AgentConfig
        let _config: Wrapper = Config::builder()
            .add_source(default)
            .build()
            .unwrap() //#[allow_ci]
            .try_deserialize()
            .unwrap(); //#[allow_ci]
    }

    #[test]
    fn test_config_get_setting() {
        let _env_config =
            config_get_setting().expect("failed to get settings");
    }

    #[test]
    fn test_config_get_file_path() {
        let workdir = Path::new("/workdir");
        let default = "default-file-name";

        let list_str = "[\"\", default, '', /absolute/path, relative/path, \"with spaces\", \"with,commas\", \"double_quotes\", 'single_quotes']";
        let list = parse_list(list_str).unwrap(); //#[allow_ci]

        assert_eq!(
            vec![
                "default",
                "/absolute/path",
                "relative/path",
                "\"with spaces\"",
                "\"with,commas\"",
                "\"double_quotes\"",
                "'single_quotes'"
            ],
            list
        );

        let translated: Vec<String> = list
            .iter()
            .map(|e| config_get_file_path("test", e, workdir, default, false))
            .collect();

        assert_eq!(
            vec![
                "/workdir/default-file-name",
                "/absolute/path",
                "/workdir/relative/path",
                "/workdir/\"with spaces\"",
                "/workdir/\"with,commas\"",
                "/workdir/\"double_quotes\"",
                "/workdir/'single_quotes'"
            ],
            translated
        );

        let translated =
            config_get_file_path("test", "", workdir, "default", true);
        assert_eq!("", translated);

        let translated =
            config_get_file_path("test", "", workdir, "default", false);
        assert_eq!("/workdir/default", translated);
    }
}
