//! Handling of common items used in configuration objects.

use std::{fmt::Display, path::PathBuf};

use garde::Validate;
use serde::{Deserialize, Serialize};

use crate::{
    Error,
    config::technology::openpgp::{OpenpgpSettings, VerificationMethod},
    file::{ConfigOpenpgpSettings, ConfigTechnologySettings, ConfigVerificationMethod},
};

/// A set of [`TechnologySettings`] that serve as defaults.
///
/// An [`TechnologySettingsDefaults`] is used when calling
/// [`TechnologySettings::from_config_with_defaults`], which creates a new
/// [`TechnologySettings`] from a [`ConfigTechnologySettings`].
/// Here, an [`TechnologySettingsDefaults`] provides defaults for any unset field in
/// [`ConfigTechnologySettings`].
///
/// # Note
///
/// When creating the [`TechnologySettings`] for an OS-level environment, the `os_defaults` field
/// should be unset. When creating the [`TechnologySettings`] for a context-level environment, the
/// `os_defaults` field should be set.
#[derive(Clone, Copy, Debug)]
pub(crate) struct TechnologySettingsDefaults<'a> {
    pub(crate) built_in_defaults: &'a TechnologySettings,
    pub(crate) os_defaults: Option<&'a TechnologySettings>,
}

impl<'a> TechnologySettingsDefaults<'a> {
    /// Returns a matching [`OpenpgpSettings`] for a [`ConfigOpenpgpSettings`].
    ///
    /// Settings are considered to be matching if their verification method is the same.
    pub(crate) fn matching_openpgp_settings(
        &self,
        config: &ConfigOpenpgpSettings,
    ) -> Option<(&OpenpgpSettings, &[ConfigOrigin])> {
        match config.config_verification_method() {
            ConfigVerificationMethod::Plain(_) => {
                if matches!(
                    self.os_defaults,
                    Some(TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::Plain(_),
                            ..
                        },
                        ..
                    })
                ) {
                    self.os_defaults
                        .map(|settings| (settings.openpgp_settings(), settings.origins()))
                } else if matches!(
                    self.built_in_defaults,
                    TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::Plain(_),
                            ..
                        },
                        ..
                    }
                ) {
                    Some((
                        self.built_in_defaults.openpgp_settings(),
                        self.built_in_defaults.origins(),
                    ))
                } else {
                    None
                }
            }
            ConfigVerificationMethod::TrustAnchor(_) => {
                if matches!(
                    self.os_defaults,
                    Some(TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::TrustAnchor(_),
                            ..
                        },
                        ..
                    })
                ) {
                    self.os_defaults
                        .map(|settings| (settings.openpgp_settings(), settings.origins()))
                } else if matches!(
                    self.built_in_defaults,
                    TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::TrustAnchor(_),
                            ..
                        },
                        ..
                    }
                ) {
                    Some((
                        self.built_in_defaults.openpgp_settings(),
                        self.built_in_defaults.origins(),
                    ))
                } else {
                    None
                }
            }
            ConfigVerificationMethod::WebOfTrust(_) => {
                if matches!(
                    self.os_defaults,
                    Some(TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::WebOfTrust(_),
                            ..
                        },
                        ..
                    })
                ) {
                    self.os_defaults
                        .map(|settings| (settings.openpgp_settings(), settings.origins()))
                } else if matches!(
                    self.built_in_defaults,
                    TechnologySettings {
                        openpgp: OpenpgpSettings {
                            verification_method: VerificationMethod::WebOfTrust(_),
                            ..
                        },
                        ..
                    }
                ) {
                    Some((
                        self.built_in_defaults.openpgp_settings(),
                        self.built_in_defaults.origins(),
                    ))
                } else {
                    None
                }
            }
        }
    }
}

/// The origin of a config file.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfigOrigin {
    /// The path to a config file.
    ConfigFile(PathBuf),

    /// The path to a drop-in config file.
    DropInFile(PathBuf),

    /// Built-in defaults.
    Default,
}

impl Display for ConfigOrigin {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ConfigFile(path) => write!(f, "Config file: {}", path.to_string_lossy()),
            Self::DropInFile(path) => {
                write!(f, "Drop-in config file: {}", path.to_string_lossy())
            }
            Self::Default => write!(f, "Built-in defaults"),
        }
    }
}

/// Settings for all supported cryptographic technologies.
///
/// Describes settings for all supported cryptographic technologies in a configuration object.
/// May be used on a system, OS or context level.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
pub struct TechnologySettings {
    #[garde(skip)]
    pub(crate) origins: Vec<ConfigOrigin>,

    #[garde(dive)]
    openpgp: OpenpgpSettings,
}

impl TechnologySettings {
    /// Creates a new [`TechnologySettings`] from an [`OpenpgpSettings`].
    pub fn new(origins: Vec<ConfigOrigin>, openpgp: OpenpgpSettings) -> Self {
        Self { origins, openpgp }
    }

    /// Returns a reference to the list of origins that were involved in the creation of the
    /// [`TechnologySettings`].
    pub fn origins(&self) -> &[ConfigOrigin] {
        &self.origins
    }

    /// Creates a new [`TechnologySettings`] from a [`ConfigTechnologySettings`] and `defaults` for
    /// any unset fields.
    ///
    /// # Errors
    ///
    /// Returns an error, if a validated [`OpenpgpSettings`] cannot be created from the `config` and
    /// the set of `defaults`.
    pub(crate) fn from_config_with_defaults(
        config: &ConfigTechnologySettings,
        defaults: TechnologySettingsDefaults,
    ) -> Result<Self, Error> {
        let (openpgp_settings, origins) =
            if let Some(config_openpgp_settings) = config.config_openpgp_settings() {
                // If the config provides OpenPGP settings, attempt to use matching OpenPGP settings
                // from the technology settings defaults to populate any unset fields.
                let (openpgp_settings_defaults, origins) = if let Some(openpgp_settings) =
                    defaults.matching_openpgp_settings(config_openpgp_settings)
                {
                    openpgp_settings
                // If no matching OpenPGP settings are found in the settings defaults, use default
                // OpenpgpSettings.
                } else {
                    (
                        &OpenpgpSettings::default(),
                        [ConfigOrigin::Default].as_slice(),
                    )
                };

                (
                    OpenpgpSettings::from_config_with_defaults(
                        config_openpgp_settings,
                        openpgp_settings_defaults,
                    )?,
                    origins,
                )
            // If the config provides no OpenPGP settings, use the OS-level defaults or fall
            // back to the system-level defaults.
            } else if let Some(os_level) = defaults.os_defaults {
                (os_level.openpgp_settings().clone(), os_level.origins())
            } else {
                (
                    defaults.built_in_defaults.openpgp_settings().clone(),
                    defaults.built_in_defaults.origins(),
                )
            };
        // Prepend the config origins.
        let origins = [config.origins(), origins].concat();

        Ok(TechnologySettings::new(origins, openpgp_settings))
    }

    /// Creates a new [`TechnologySettings`] from a YAML string.
    pub fn from_yaml_str(s: &str) -> Result<Self, Error> {
        let settings: Self =
            serde_saphyr::from_str(s).map_err(|source| Error::YamlDeserialize {
                context: "deserializing a YAML-based system-level configuration string".to_string(),
                source,
            })?;

        settings.validate().map_err(|source| Error::Validation {
            context: "creating technology settings from a YAML string".to_string(),
            source,
        })?;

        Ok(settings)
    }

    /// Returns a reference to the tracked [`OpenpgpSettings`].
    pub fn openpgp_settings(&self) -> &OpenpgpSettings {
        &self.openpgp
    }
}

impl Default for TechnologySettings {
    fn default() -> Self {
        Self {
            origins: vec![ConfigOrigin::Default],
            openpgp: OpenpgpSettings::default(),
        }
    }
}

impl Display for TechnologySettings {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}", self.openpgp_settings())?;

        if !self.origins().is_empty() {
            writeln!(
                f,
                "📝 The following sources have been considered for the creation of the settings:"
            )?;
            for origin in self.origins().iter() {
                writeln!(f, "⤷ {origin}")?;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::{collections::HashSet, num::NonZeroUsize, thread::current};

    use insta::{assert_snapshot, with_settings};
    use rstest::rstest;
    use testresult::TestResult;

    use super::*;
    use crate::{
        file::{ConfigTrustAnchorMode, ConfigWebOfTrustMode},
        openpgp::{
            NumCertifications,
            NumDataSignatures,
            PlainMode,
            TrustAmountFlow,
            TrustAmountPartial,
            TrustAmountRoot,
            TrustAnchorMode,
            WebOfTrustMode,
            WebOfTrustRoot,
        },
    };

    const SNAPSHOT_PATH: &str = "fixtures/settings/";

    /// Ensures that [`TechnologySettings::from_config_with_defaults`] succeeds.
    #[rstest]
    #[case::config_without_openpgp_settings_overriden_with_system_level(
        ConfigTechnologySettings::new(
            vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
            None,
        ),
        TechnologySettingsDefaults{
            built_in_defaults: &TechnologySettings::new(
                vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            ),
            os_defaults: None,
        },
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
            ],
            OpenpgpSettings::new(
                NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
                VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
            )?
        )
    )]
    #[case::config_without_openpgp_settings_overriden_with_os_level(
        ConfigTechnologySettings::new(
            vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
            None,
        ),
        TechnologySettingsDefaults{
            built_in_defaults: &TechnologySettings::new(
                vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            ),
            os_defaults: Some(&TechnologySettings::new(
                vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            )),
        },
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
            ],
            OpenpgpSettings::new(
                NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
                VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
            )?
        )
    )]
    #[case::config_with_wot_openpgp_settings_overridden_by_generic_defaults(
        ConfigTechnologySettings::new(
            vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
            Some(ConfigOpenpgpSettings::new(
                Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
                ConfigVerificationMethod::WebOfTrust(ConfigWebOfTrustMode::new(None, None, Some(HashSet::new()), Some(HashSet::new())))
            )?
        )),
        TechnologySettingsDefaults{
            built_in_defaults: &TechnologySettings::new(
                vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            ),
            os_defaults: Some(&TechnologySettings::new(
                vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            )),
        },
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::WebOfTrust(
                    WebOfTrustMode::new(
                        TrustAmountFlow::default(),
                        TrustAmountPartial::default(),
                        HashSet::new(),
                        HashSet::new(),
                    )
                )
            )?
        )
    )]
    #[case::config_with_trust_anchor_openpgp_settings_overridden_by_trust_anchor_defaults(
        ConfigTechnologySettings::new(
            vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
            Some(ConfigOpenpgpSettings::new(
                Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
                ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(None, Some(HashSet::new()), Some(HashSet::new()))?)
            )?
        )),
        TechnologySettingsDefaults{
            built_in_defaults: &TechnologySettings::new(
                vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            ),
            os_defaults: Some(&TechnologySettings::new(
                vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
                OpenpgpSettings::new(
                    NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
                    VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
                )?
            )),
        },
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::TrustAnchor(
                    TrustAnchorMode::new(NumCertifications::default(), HashSet::new(), HashSet::new())?
                )
            )?
        )
    )]
    fn technology_settings_from_config_with_defaults_succeeds(
        #[case] config: ConfigTechnologySettings,
        #[case] defaults: TechnologySettingsDefaults,
        #[case] expected: TechnologySettings,
    ) -> TestResult {
        let settings = TechnologySettings::from_config_with_defaults(&config, defaults)?;
        assert_eq!(settings, expected);

        Ok(())
    }

    /// Ensures that [`TechnologySettings::from_yaml_str`] fails on invalid YAML.
    #[test]
    fn technology_settings_from_yaml_str_fails_on_invalid_yaml() {
        let broken_yaml = "foo:\n  bar:";

        match TechnologySettings::from_yaml_str(broken_yaml) {
            Err(Error::YamlDeserialize { .. }) => {}
            Err(error) => {
                panic!(
                    "Expected an Error::YamlDeserialize but failed with a different error: {error}"
                )
            }
            Ok(settings) => {
                panic!("Expected an Error::YamlDeserialize but succeeded: {settings:?}")
            }
        }
    }

    #[test]
    fn technology_settings_from_yaml_str_fails_on_validation() {
        let invalid_settings = r#"origins:
  - config_file: /usr/share/voa/example.yaml
openpgp:
  num_data_signatures: 2
  verification_method:
    plain:
      identity_domain_matches:
        - example.org
      fingerprint_matches:
        - d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f
"#;

        match TechnologySettings::from_yaml_str(invalid_settings) {
            Err(Error::Validation { .. }) => {}
            Err(error) => {
                panic!("Expected an Error::Validation but failed with a different error: {error}")
            }
            Ok(settings) => {
                panic!("Expected an Error::Validation but succeeded: {settings:?}")
            }
        }
    }

    /// Ensures that the string representation of [`TechnologySettings`] remains consistent.
    #[rstest]
    #[case::plain_mode_empty_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
            )?
        )
    )]
    #[case::plain_mode_custom_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::Plain(PlainMode::new(
                    HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
                    HashSet::from_iter([
                        "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                        "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                    ]),
                ))
            )?
        )
    )]
    #[case::trust_anchor_empty_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::TrustAnchor(
                    TrustAnchorMode::new(
                        NumCertifications::default(),
                        HashSet::new(),
                        HashSet::new(),
                    )?
                )
            )?
        )
    )]
    #[case::trust_anchor_custom_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::TrustAnchor(
                    TrustAnchorMode::new(
                        NumCertifications::default(),
                        HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
                        HashSet::from_iter([
                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                            "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
                        ]),
                    )?
                )
            )?
        )
    )]
    #[case::wot_empty_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::WebOfTrust(
                    WebOfTrustMode::new(
                        TrustAmountFlow::default(),
                        TrustAmountPartial::default(),
                        HashSet::new(),
                        HashSet::new(),
                    )
                )
            )?
        )
    )]
    #[case::wot_custom_lists(
        TechnologySettings::new(
            vec![
                ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
                ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::WebOfTrust(
                    WebOfTrustMode::new(
                        TrustAmountFlow::default(),
                        TrustAmountPartial::default(),
                        HashSet::from_iter([
                            WebOfTrustRoot::new(
                                "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                                TrustAmountRoot::default(),
                            ),
                            WebOfTrustRoot::new(
                                "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                                TrustAmountRoot::default(),
                            ),
                        ]),
                        HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
                    )
                )
            )?
        )
    )]
    fn technology_settings_display(#[case] settings: TechnologySettings) -> TestResult {
        let description =
            "Technology settings with OpenPGP settings that use various custom settings.";
        let settings_str = settings.to_string();

        with_settings!({
            description => description,
            snapshot_path => SNAPSHOT_PATH,
            prepend_module_to_snapshot => false,
        }, {
            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), settings_str);
        });

        Ok(())
    }

    /// Ensures that the default string representation of [`VerificationMethod`] used in
    /// [`TechnologySettings`] remains consistent.
    #[rstest]
    #[case::plain_mode(
        TechnologySettings::new(
            vec![
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::Plain(PlainMode::default())
            )?
        )
    )]
    #[case::trust_anchor(
        TechnologySettings::new(
            vec![
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::TrustAnchor(TrustAnchorMode::default())
            )?
        )
    )]
    #[case::wot(
        TechnologySettings::new(
            vec![
                ConfigOrigin::Default,
            ],
            OpenpgpSettings::new(
                NumDataSignatures::default(),
                VerificationMethod::WebOfTrust(WebOfTrustMode::default())
            )?
        )
    )]
    fn technology_settings_verification_method_defaults(
        #[case] settings: TechnologySettings,
    ) -> TestResult {
        let description = "Technology settings with OpenPGP settings that use the default settings for each verification method.";
        let settings_str = settings.to_string();

        with_settings!({
            description => description,
            snapshot_path => SNAPSHOT_PATH,
            prepend_module_to_snapshot => false,
        }, {
            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), settings_str);
        });

        Ok(())
    }
}
