/*
 * This file is part of CycloneDX Rust Cargo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::external_models::{normalized_string::NormalizedString, uri::Uri};
use crate::validation::{Validate, ValidationContext, ValidationResult};

/// Defines a source related to the vulnerability, e.g. who published or calculated the severity or risk rating the vulnerability.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_vulnerabilitySourceType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilitySource {
    pub name: Option<NormalizedString>,
    pub url: Option<Uri>,
}

impl VulnerabilitySource {
    /// Construct a `VulnerabilitySource` with a name and `Uri`
    /// ```
    /// use cyclonedx_bom::external_models::uri::{Uri, UriError};
    /// use cyclonedx_bom::models::vulnerability_source::VulnerabilitySource;
    /// use std::convert::TryFrom;
    ///
    /// let uri = Uri::try_from("https://example.com".to_string())?;
    /// let source = VulnerabilitySource::new(Some("Example Org".to_string()), Some(uri));
    /// # Ok::<(), UriError>(())
    /// ```
    pub fn new(name: Option<String>, url: Option<Uri>) -> Self {
        match name {
            None => Self { name: None, url },
            Some(name) => Self {
                name: Some(NormalizedString::new(&name)),
                url,
            },
        }
    }
}

impl Validate for VulnerabilitySource {
    fn validate_with_context(&self, context: ValidationContext) -> ValidationResult {
        let mut results: Vec<ValidationResult> = vec![];

        if let Some(name) = &self.name {
            let context = context.with_struct("VulnerabilitySource", "name");

            results.push(name.validate_with_context(context));
        }

        if let Some(url) = &self.url {
            let context = context.with_struct("VulnerabilitySource", "url");

            results.push(url.validate_with_context(context));
        }

        results
            .into_iter()
            .fold(ValidationResult::default(), |acc, result| acc.merge(result))
    }
}

#[cfg(test)]
mod test {
    use crate::{
        external_models::uri::Uri,
        validation::{FailureReason, ValidationPathComponent},
    };

    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn valid_vulnerability_source_should_pass_validation() {
        let validation_result = VulnerabilitySource {
            name: Some(NormalizedString::new("name")),
            url: Some(Uri("url".to_string())),
        }
        .validate();

        assert_eq!(validation_result, ValidationResult::Passed);
    }

    #[test]
    fn invalid_vulnerability_source_should_fail_validation() {
        let validation_result = VulnerabilitySource {
            name: Some(NormalizedString("invalid\tname".to_string())),
            url: Some(Uri("invalid url".to_string())),
        }
        .validate();

        assert_eq!(
            validation_result,
            ValidationResult::Failed {
                reasons: vec![
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![ValidationPathComponent::Struct {
                            struct_name: "VulnerabilitySource".to_string(),
                            field_name: "name".to_string()
                        },])
                    },
                    FailureReason {
                        message: "Uri does not conform to RFC 3986".to_string(),
                        context: ValidationContext(vec![ValidationPathComponent::Struct {
                            struct_name: "VulnerabilitySource".to_string(),
                            field_name: "url".to_string()
                        },])
                    },
                ]
            }
        );
    }
}
