/*
 * 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::validate_normalized_string;
use crate::external_models::uri::validate_uri;
use crate::external_models::{normalized_string::NormalizedString, uri::Uri};
use crate::validation::{Validate, ValidationContext, ValidationResult};

use super::bom::SpecVersion;

/// 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_version(&self, _version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_field_option("name", self.name.as_ref(), validate_normalized_string)
            .add_field_option("url", self.url.as_ref(), validate_uri)
            .into()
    }
}

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

    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!(validation_result.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,
            vec![
                validation::field(
                    "name",
                    "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                ),
                validation::field("url", "Uri does not conform to RFC 3986"),
            ]
            .into()
        );
    }
}
