// SPDX-FileCopyrightText: 2023 Greenbone AG
//
// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception

use std::{
    collections::HashMap,
    fmt::{self, Display, Formatter},
    net::SocketAddr,
    path::PathBuf,
    str::FromStr,
    time::Duration,
};

use crate::container_image_scanner::config::{DBLocation, SqliteConfiguration};
use clap::{ArgAction, builder::TypedValueParser};
use logging::SerLevel;
use scannerlib::{
    models::PreferenceValue,
    scanner::preferences::preference::{PREFERENCES, ScanPrefValue},
};
use serde::{Deserialize, Serialize};
mod logging;

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Feed {
    pub path: PathBuf,
    #[serde(
        deserialize_with = "scannerlib::utils::duration::deserialize",
        serialize_with = "scannerlib::utils::duration::serialize"
    )]
    pub check_interval: Duration,

    // TODO: rename to feed_integrity_check and tell serde to accept:
    // signature_check as weel as feed_integrity_check.
    pub signature_check: bool,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Notus {
    pub products_path: PathBuf,
    pub advisories_path: PathBuf,
    pub address: Option<SocketAddr>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Scheduler {
    #[serde(default)]
    pub max_queued_scans: Option<usize>,
    #[serde(default)]
    pub max_running_scans: Option<usize>,
    #[serde(default)]
    pub min_free_mem: Option<u64>,
    #[serde(
        deserialize_with = "scannerlib::utils::duration::deserialize",
        serialize_with = "scannerlib::utils::duration::serialize"
    )]
    pub check_interval: Duration,
}

impl Default for Scheduler {
    fn default() -> Self {
        Self {
            check_interval: Duration::from_secs(2),
            max_queued_scans: None,
            max_running_scans: None,
            min_free_mem: None,
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(default)]
pub struct Scanner {
    #[serde(rename = "type")]
    pub scanner_type: ScannerType,
    pub ospd: OspdWrapper,
    pub preferences: HashMap<String, ScanPrefValue>,
}

#[derive(Default, Deserialize, Serialize, Debug, Clone)]
pub enum ScannerType {
    #[serde(rename = "ospd")]
    #[default]
    Ospd,
    #[serde(rename = "openvas")]
    Openvas,
    #[serde(rename = "openvasd")]
    Openvasd,
}

impl TypedValueParser for ScannerType {
    type Value = ScannerType;

    fn parse_ref(
        &self,
        cmd: &clap::Command,
        _: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        Ok(match value.to_str().unwrap_or_default() {
            "ospd" => ScannerType::Ospd,
            "openvas" => ScannerType::Openvas,
            "openvasd" => ScannerType::Openvasd,
            x => {
                let mut cmd = cmd.clone();
                let err = cmd.error(
                    clap::error::ErrorKind::InvalidValue,
                    format!("`{x}` is not a scanner type."),
                );
                return Err(err);
            }
        })
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct OspdWrapper {
    pub socket: PathBuf,
    pub read_timeout: Option<Duration>,
}

impl Default for OspdWrapper {
    fn default() -> Self {
        OspdWrapper {
            socket: PathBuf::from("/var/run/ospd/ospd-openvas.sock"),
            read_timeout: None,
        }
    }
}

impl Default for Feed {
    fn default() -> Self {
        Feed {
            path: PathBuf::from("/var/lib/openvas/plugins"),
            check_interval: Duration::from_secs(3600),
            signature_check: true,
        }
    }
}

impl Default for Notus {
    fn default() -> Self {
        Notus {
            products_path: PathBuf::from("/var/lib/notus/products"),
            advisories_path: PathBuf::from("/var/lib/notus/advisories"),
            address: None,
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Listener {
    pub address: SocketAddr,
}

impl Default for Listener {
    fn default() -> Self {
        Self {
            address: ([127, 0, 0, 1], 3000).into(),
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
/// Describes different modes openvasd can be run as.
///
/// `Service` prefix means that openvasd is a http service used by others.
/// `Client` prefix means that openvasd is run as a client and  calls other services.
pub enum Mode {
    #[default]
    #[serde(rename = "service")]
    /// Enables all endpoints and monitors feed and active scans.
    Service,
    /// Disables everything but the notus endpoints
    #[serde(rename = "service_notus")]
    ServiceNotus,
}

impl TypedValueParser for Mode {
    type Value = Self;

    fn parse_ref(
        &self,
        cmd: &clap::Command,
        _arg: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        Ok(match value.to_str().unwrap_or_default() {
            "service" => Self::Service,
            "service_notus" => Self::ServiceNotus,
            _ => {
                let mut cmd = cmd.clone();
                let err = cmd.error(
                    clap::error::ErrorKind::InvalidValue,
                    "`{}` is not a scanner type.",
                );
                return Err(err);
            }
        })
    }
}

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Endpoints {
    pub enable_get_scans: bool,
    #[serde(default)]
    pub enable_get_performance: Option<bool>,
    #[serde(default)]
    pub key: Option<String>,
}

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Tls {
    pub certs: Option<PathBuf>,
    pub key: Option<PathBuf>,
    pub client_certs: Option<PathBuf>,
}

#[derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq, Eq)]
pub enum StorageType {
    #[default]
    #[serde(rename = "inmemory")]
    InMemory,
    #[serde(rename = "fs")]
    FileSystem,
    #[serde(rename = "redis")]
    Redis,
}

impl TypedValueParser for StorageType {
    type Value = StorageType;

    fn parse_ref(
        &self,
        cmd: &clap::Command,
        _: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        Ok(match value.to_str().unwrap_or_default() {
            "fs" => StorageType::FileSystem,
            "inmemory" => StorageType::InMemory,
            "redis" => StorageType::Redis,
            x => {
                let mut cmd = cmd.clone();
                let err = cmd.error(
                    clap::error::ErrorKind::InvalidValue,
                    format!("`{x}` is not a storage type."),
                );
                return Err(err);
            }
        })
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FileStorage {
    pub path: PathBuf,
    pub key: Option<String>,
}

impl Default for FileStorage {
    fn default() -> Self {
        Self {
            path: PathBuf::from("/var/lib/openvasd/storage"),
            key: None,
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum StorageTypes {
    V1(StorageV1),
    V2(SqliteConfiguration),
}

impl StorageTypes {
    pub fn credential_key(&self) -> Option<&str> {
        match self {
            StorageTypes::V1(storage_v1) => storage_v1.fs.key.as_deref(),
            StorageTypes::V2(sqlite_configuration) => {
                sqlite_configuration.credential_key.as_deref()
            }
        }
    }
    fn set_credential_key<S: Into<String>>(&mut self, key: S) {
        let key = key.into();
        match self {
            StorageTypes::V1(storage_v1) => storage_v1.fs.key = Some(key),
            StorageTypes::V2(sqlite_configuration) => {
                sqlite_configuration.credential_key = Some(key)
            }
        }
    }

    pub fn max_connections(&self) -> usize {
        let result = match self {
            StorageTypes::V1(_) => SqliteConfiguration::default().max_connections,
            StorageTypes::V2(sqlite_configuration) => sqlite_configuration.max_connections,
        };
        if result > 0 { result as usize } else { 1 }
    }
}
impl Default for StorageTypes {
    fn default() -> Self {
        Self::V2(Default::default())
    }
}

#[derive(Deserialize, Serialize, Default, Debug, Clone)]
pub struct StorageV1 {
    #[serde(rename = "type")]
    pub storage_type: StorageType,
    #[serde(default)]
    pub fs: FileStorage,
}

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(default)]
pub struct Config {
    pub mode: Mode,
    pub feed: Feed,
    pub notus: Notus,
    pub endpoints: Endpoints,
    pub tls: Tls,
    pub listener: Listener,
    #[serde(alias = "log", alias = "logging")]
    pub logging: logging::Logging,
    pub storage: StorageTypes,
    pub scheduler: Scheduler,
    pub scanner: Scanner,
    #[serde(alias = "container-image-scanner")]
    pub container_image_scanner: crate::container_image_scanner::Config,
    #[serde(skip)]
    pub version: bool,
}

impl Display for Config {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", toml::to_string_pretty(self).unwrap_or_default())
    }
}

impl Config {
    fn read_to_string<P>(path: P) -> String
    where
        P: AsRef<std::path::Path> + std::fmt::Display + std::fmt::Debug,
    {
        std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("Failed to read openvasd config from file: {path:?}. {e}"))
    }

    fn load_etc() -> Option<Self> {
        let path = "/etc/openvasd/openvasd.toml";
        if !std::fs::exists(path).unwrap_or_default() {
            return None;
        }
        let config = Self::read_to_string(path);
        toml::from_str(&config).unwrap()
    }

    fn load_user() -> Option<Self> {
        match std::env::var("HOME") {
            Ok(home) => {
                let path = format!("{home}/.config/openvasd/openvasd.toml");
                if !std::fs::exists(&path).unwrap_or_default() {
                    return None;
                }
                let config = Self::read_to_string(path);
                toml::from_str(&config).unwrap()
            }
            Err(_) => None,
        }
    }

    fn from_file<P>(path: P) -> Self
    where
        P: AsRef<std::path::Path> + std::fmt::Display + std::fmt::Debug,
    {
        let config = std::fs::read_to_string(path).unwrap();

        toml::from_str(&config).unwrap()
    }

    fn ret_pref_value_or_panic_with_wrong_type(
        p: &ScanPrefValue,
        expected: &str,
        name: &str,
    ) -> ScanPrefValue {
        match (p, expected) {
            (ScanPrefValue::Bool(_), "bool") => p.clone(),
            (ScanPrefValue::Int(_), "int") => p.clone(),
            (ScanPrefValue::String(_), "string") => p.clone(),
            _ => panic!("Wrong value type. Expected {expected} for {name}"),
        }
    }

    pub fn load() -> Self {
        let mut cmds = clap::Command::new("openvasd")
            .arg(
                clap::Arg::new("config")
                    .short('c')
                    .env("OPENVASD_CONFIG")
                    .long("config")
                    .action(ArgAction::Set)
                    .help("path to toml config file"),
            )
            .arg(
                clap::Arg::new("feed-path")
                    .env("FEED_PATH")
                    .long("feed-path")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("path to openvas feed"),
            )
            .arg(
                clap::Arg::new("feed-signature-check")
                    .long("feed-signature-check")
                    .short('x')
                    .action(ArgAction::SetTrue)
                    .help("Deprecated. To enable or disable feed signature use the configuration."),
            )
            .arg(
                clap::Arg::new("feed-check-interval")
                    .env("FEED_CHECK_INTERVAL")
                    .long("feed-check-interval")
                    .value_parser(clap::value_parser!(u64))
                    .value_name("SECONDS")
                    .help("interval to check for feed updates in seconds"),
            )
            .arg(
                clap::Arg::new("notus-advisories")
                    .env("NOTUS_ADVISORIES")
                    .long("advisories")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("Path containing the Notus advisories directory"))
            .arg(
                clap::Arg::new("notus-products")
                    .env("NOTUS_PRODUCTS")
                    .long("products")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("Path containing the Notus products directory"))
            .arg(
                clap::Arg::new("notus-address")
                    .env("NOTUS_ADDRESS")
                    .long("notus-address")
                    .value_name("IP:PORT")
                    .value_parser(clap::value_parser!(SocketAddr))
                    .action(ArgAction::Set)
                    .help("the address to reach notus on"))
            .arg(
                clap::Arg::new("redis-url")
                    .long("redis-url")
                    .env("REDIS_URL")
                    //.value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("Redis url. Either unix:// or redis://"))
            .arg(
                clap::Arg::new("tls-certs")
                    .env("TLS_CERTS")
                    .long("tls-certs")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("path to server tls certs"),
            )
            .arg(
                clap::Arg::new("tls-key")
                    .env("TLS_KEY")
                    .long("tls-key")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("path to server tls key"),
            )
            .arg(
                clap::Arg::new("tls-client-certs")
                    .env("TLS_CLIENT_CERTS")
                    .long("tls-client-certs")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .action(ArgAction::Set)
                    .help("path to client tls certs. Enables mtls."),
            )
            .arg(
                clap::Arg::new("enable-get-scans")
                    .env("ENABLE_GET_SCANS")
                    .long("enable-get-scans")
                    .num_args(0..=1)
                    .value_parser(clap::builder::BoolValueParser::new())
                    .default_missing_value("true")
                    .help("enable get scans endpoint. Default 'true'."),
            )
            .arg(
                clap::Arg::new("enable-get-performance")
                    .env("ENABLE_GET_PERFORMANCE")
                    .long("enable-get-performance")
                    .num_args(0..=1)
                    .value_parser(clap::builder::BoolValueParser::new())
                    .default_missing_value("false")
                    .help("enable get performance endpoint. Default 'false'."),
            )
            .arg(
                clap::Arg::new("api-key")
                    .env("API_KEY")
                    .long("api-key")
                    .action(ArgAction::Set)
                    .help("API key that must be set as X-API-KEY header to gain access"),
            )
            .arg(
                clap::Arg::new("scanner-type")
                    .env("SCANNER_TYPE")
                    .long("scanner-type")
                    .value_name("ospd,openvas")
                    .value_parser(ScannerType::Ospd)
                    .help("Type of scanner used to manage scans")
            )
            .arg(
                clap::Arg::new("max-queued-scans")
                    .env("MAX_QUEUED_SCANS")
                    .long("max-queued-scans")
                    .action(ArgAction::Set)
                    .help("Maximum number of queued scans")
            )
            .arg(
                clap::Arg::new("max-running-scans")
                    .env("MAX_RUNNING_SCANS")
                    .long("max-running-scans")
                    .action(ArgAction::Set)
                    .help("Maximum number of active running scans, omit for no limits")

            )
            .arg(
                clap::Arg::new("min-free-mem")
                    .env("MIN_FREE_MEMORY")
                    .long("min-free-mem")
                    .action(ArgAction::Set)
                    .help("Minimum memory available to start a new scan")
            )
            .arg(
                clap::Arg::new("check-interval")
                    .env("SCHEDULER_CHECK_INTERVAL")
                    .long("check_interval")
                    .value_parser(clap::value_parser!(u64))
                    .value_name("SECONDS")
                    .help("Check interval of the Scheduler if a new scan can be started")
            )
            .arg(
                clap::Arg::new("ospd-socket")
                    .env("OSPD_SOCKET")
                    .long("ospd-socket")
                    .help("socket to ospd")
                    .value_parser(clap::builder::PathBufValueParser::new()),
            )
            .arg(
                clap::Arg::new("read-timeout")
                    .env("READ_TIMEOUT")
                    .long("read-timeout")
                    .value_parser(clap::value_parser!(u64))
                    .value_name("SECONDS")
                    .help("read timeout in seconds on the ospd-openvas socket"),
            )
            .arg(
                clap::Arg::new("result-check-interval")
                    .env("RESULT_CHECK_INTERVAL")
                    .long("result-check-interval")
                    .value_parser(clap::value_parser!(u64))
                    .value_name("SECONDS")
                    .help("interval to check for new results in seconds"),
            )
            .arg(
                clap::Arg::new("listening")
                    .env("LISTENING")
                    .long("listening")
                    .short('l')
                    .value_name("IP:PORT")
                    .value_parser(clap::value_parser!(SocketAddr))
                    .help("the address to listen to (e.g. 127.0.0.1:3000 or 0.0.0.0:3000)."),
            )
            .arg(
                clap::Arg::new("storage_type")
                    .env("STORAGE_TYPE")
                    .long("storage-type")
                    .value_parser(StorageType::InMemory)
                    .value_name("redis,inmemory,fs")
                    .help("either be stored in memory or on the filesystem."),
            )
            .arg(
                clap::Arg::new("storage_path")
                    .env("STORAGE_PATH")
                    .long("storage-path")
                    .value_name("PATH")
                    .value_parser(clap::builder::PathBufValueParser::new())
                    .help("the path that contains the files when type is set to fs."),
            )
            .arg(
                clap::Arg::new("storage_key")
                    .env("STORAGE_KEY")
                    .long("storage-key")
                    .value_name("KEY")
                    .help("the password to use for encryption when type is set to fs. If not set the files are not encrypted."),
            )
            .arg(
                clap::Arg::new("log-level")
                    .env("OPENVASD_LOG")
                    .long("log-level")
                    .short('L')
                    .help("Level of log messages to be shown. TRACE > DEBUG > INFO > WARN > ERROR"),
            )
            .arg(
                clap::Arg::new("version")
                    .long("version")
                    .action(ArgAction::SetTrue)
                    .help("Show openvasd version and exit."),
            )
            .arg(
                clap::Arg::new("mode")
                    .env("OPENVASD_MODE")
                    .long("mode")
                    .value_name("service,service_notus")
                    .value_parser(Mode::Service)
                    .help("Sets the openvasd mode"),
            );

        for pref in PREFERENCES {
            let mut arg = clap::Arg::new(pref.id).long(pref.id).help(pref.description);
            arg = match pref.default {
                PreferenceValue::Bool(_) => arg.value_parser(clap::builder::BoolValueParser::new()),
                PreferenceValue::String(_) => arg.value_parser(clap::value_parser!(String)),
                PreferenceValue::Int(_) => arg.value_parser(clap::value_parser!(i64)),
            };
            cmds = cmds.arg(arg);
        }

        let cmds = cmds.get_matches();
        let mut config = match cmds.get_one::<String>("config") {
            Some(path) => Self::from_file(path),
            None => {
                if let Some(config) = Self::load_user() {
                    config
                } else {
                    Self::load_etc().unwrap_or_default()
                }
            }
        };

        if let Some(interval) = cmds.get_one::<u64>("feed-check-interval") {
            config.feed.check_interval = Duration::from_secs(*interval);
        }
        if let Some(scanner_type) = cmds.get_one::<ScannerType>("scanner-type") {
            config.scanner.scanner_type = scanner_type.clone()
        }
        if let Some(max_queued_scans) = cmds.get_one::<usize>("max-queued-scans") {
            config.scheduler.max_queued_scans = Some(*max_queued_scans)
        }
        if let Some(max_running_scans) = cmds.get_one::<usize>("max-running-scans") {
            config.scheduler.max_running_scans = Some(*max_running_scans)
        }
        if let Some(min_free_mem) = cmds.get_one::<u64>("min-free-mem") {
            config.scheduler.min_free_mem = Some(*min_free_mem)
        }
        if let Some(check_interval) = cmds.get_one::<u64>("check-interval") {
            config.scheduler.check_interval = Duration::from_millis(*check_interval)
        }
        if let Some(path) = cmds.get_one::<PathBuf>("ospd-socket") {
            config.scanner.ospd.socket.clone_from(path);
        }
        if let Some(interval) = cmds.get_one::<u64>("read-timeout") {
            config.scanner.ospd.read_timeout = Some(Duration::from_secs(*interval));
        }

        if let Some(path) = cmds.get_one::<PathBuf>("feed-path") {
            config.feed.path.clone_from(path);
        }
        if let Some(path) = cmds.get_one::<PathBuf>("notus-products") {
            config.notus.products_path.clone_from(path);
        }
        if let Some(path) = cmds.get_one::<PathBuf>("notus-advisories") {
            config.notus.advisories_path.clone_from(path);
        }
        if let Some(address) = cmds.get_one::<SocketAddr>("notus-address") {
            config.notus.address = Some(*address);
        }
        if let Some(_path) = cmds.get_one::<String>("redis-url") {
            // is actually ignored as on scanner openvas the redis-url of openvas is used
        }
        if let Some(path) = cmds.get_one::<PathBuf>("tls-certs") {
            config.tls.certs = Some(path.clone());
        }
        if let Some(path) = cmds.get_one::<PathBuf>("tls-key") {
            config.tls.key = Some(path.clone());
        }
        if let Some(path) = cmds.get_one::<PathBuf>("tls-client-certs") {
            config.tls.client_certs = Some(path.clone());
        }
        if let Some(enable) = cmds.get_one::<bool>("enable-get-scans") {
            config.endpoints.enable_get_scans = *enable;
        }
        if let Some(version) = cmds.get_one::<bool>("version") {
            config.version = *version;
        }
        if let Some(enable) = cmds.get_one::<bool>("enable-get-performance") {
            config.endpoints.enable_get_performance = Some(*enable);
        }
        if let Some(api_key) = cmds.get_one::<String>("api-key") {
            config.endpoints.key = Some(api_key.clone());
        }
        if let Some(ip) = cmds.get_one::<SocketAddr>("listening") {
            config.listener.address = *ip;
        }
        if let Some(log_level) = cmds.get_one::<String>("log-level") {
            config.logging.level = SerLevel::from_str(log_level).unwrap_or_default();
        }
        if let Some(stype) = cmds.get_one::<StorageType>("storage_type") {
            config.storage = StorageTypes::V2(match stype {
                StorageType::InMemory | StorageType::Redis => SqliteConfiguration::default(),
                StorageType::FileSystem => SqliteConfiguration::default_file_location("openvasd"),
            });

            // although openvasd and container-image-scanner can be configured separately we just
            // allow one configuration to net get the arguments even more cluttered.
            config.container_image_scanner.database = match stype {
                StorageType::InMemory | StorageType::Redis => SqliteConfiguration::default(),
                StorageType::FileSystem => {
                    SqliteConfiguration::default_file_location("container-image-scanner")
                }
            };
        }
        if let Some(path) = cmds.get_one::<PathBuf>("storage_path") {
            //TODO: check if dir
            config.storage = StorageTypes::V2(SqliteConfiguration {
                location: DBLocation::File(path.clone()),
                ..Default::default()
            });
            // TODO: add suffix
            config.container_image_scanner.database = SqliteConfiguration {
                location: DBLocation::File(path.clone()),
                ..Default::default()
            };
        }
        if let Some(mode) = cmds.get_one::<Mode>("mode") {
            config.mode = mode.clone();
        }
        if let Some(key) = cmds.get_one::<String>("storage_key")
            && !key.is_empty()
        {
            config.storage.set_credential_key(key.clone());
        }

        let scan_prefs: HashMap<String, ScanPrefValue> = PREFERENCES
            .iter()
            .map(|pref| {
                let val = match pref.default {
                    PreferenceValue::Bool(_) => {
                        if let Some(p) = cmds.get_one::<bool>(pref.id) {
                            ScanPrefValue::Bool(*p)
                        } else if let Some(p) = config.scanner.preferences.get(pref.id) {
                            Self::ret_pref_value_or_panic_with_wrong_type(p, "bool", pref.id)
                        } else {
                            pref.default.clone().into()
                        }
                    }
                    PreferenceValue::String(_) => {
                        if let Some(p) = cmds.get_one::<String>(pref.id) {
                            ScanPrefValue::String(p.to_string())
                        } else if let Some(p) = config.scanner.preferences.get(pref.id) {
                            Self::ret_pref_value_or_panic_with_wrong_type(p, "string", pref.id)
                        } else {
                            pref.default.clone().into()
                        }
                    }
                    PreferenceValue::Int(_) => {
                        if let Some(p) = cmds.get_one::<i64>(pref.id) {
                            ScanPrefValue::Int(*p)
                        } else if let Some(p) = config.scanner.preferences.get(pref.id) {
                            Self::ret_pref_value_or_panic_with_wrong_type(p, "int", pref.id)
                        } else {
                            pref.default.clone().into()
                        }
                    }
                };
                (pref.id.to_string(), val)
            })
            .collect();

        config.scanner.preferences = scan_prefs;
        config
    }
}

#[cfg(test)]
mod tests {

    use crate::{config::StorageTypes, container_image_scanner::config::ImageExtractionLocation};
    use insta::assert_toml_snapshot;
    use scannerlib::scanner::preferences::preference::ScanPrefValue;

    #[test]
    fn current_example_parseable() {
        let mut path = env!("CARGO_MANIFEST_DIR").to_string();
        path.push_str("/examples/openvasd/config.example.toml");
        let content = std::fs::read_to_string(&path).unwrap();

        let storage_test = toml::from_str::<super::Config>(&content).unwrap();
        assert!(matches!(storage_test.storage, StorageTypes::V2(_)));
    }

    #[test]
    fn downards_compatible() {
        let mut path = env!("CARGO_MANIFEST_DIR").to_string();
        path.push_str("/examples/openvasd/config.example_v1.toml");

        let content = std::fs::read_to_string(&path).unwrap();

        let storage_test = toml::from_str::<super::Config>(&content).unwrap();
        assert!(matches!(storage_test.storage, StorageTypes::V1(_)));
    }

    #[test]
    fn defaults() {
        let mut config = super::Config::default();

        // just adding one otherwise we could run into order issues
        config
            .scanner
            .preferences
            .insert("aaa".to_string(), ScanPrefValue::Bool(false));

        // we hardcode that here, otherwise the test may fail on machines that have different
        // XDG_PATHs set.
        config.container_image_scanner.image.extract_to =
            ImageExtractionLocation::File("/tmp/openvasd/cis".into());

        assert_toml_snapshot!(config);
    }
}
