/*
 * 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, NormalizedString};
use crate::models::vulnerability_source::VulnerabilitySource;
use crate::validation::{Validate, ValidationContext, ValidationResult};

use super::bom::SpecVersion;

/// References a vulnerability equivalent to the vulnerability specified, e.g.
/// to correlate vulnerabilities across multiple sources of vulnerability intelligence.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_vulnerabilityType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityReference {
    pub id: NormalizedString,
    pub vulnerability_source: VulnerabilitySource,
}

/// Construct a `VulnerabilityReference` with an id and `VulnerabilitySource`
/// ```
/// use cyclonedx_bom::external_models::uri::{Uri, UriError};
/// use cyclonedx_bom::models::vulnerability_reference::VulnerabilityReference;
/// 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));
/// let reference = VulnerabilityReference::new("12a34a5b-6780-1bae-2345-67890cfe12a3", source);
/// # Ok::<(), UriError>(())
/// ```
impl VulnerabilityReference {
    pub fn new(id: &str, source: VulnerabilitySource) -> Self {
        Self {
            id: NormalizedString::new(id),
            vulnerability_source: source,
        }
    }
}

impl Validate for VulnerabilityReference {
    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_field("id", &self.id, validate_normalized_string)
            .add_struct("vulnerability_source", &self.vulnerability_source, version)
            .into()
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityReferences(pub Vec<VulnerabilityReference>);

impl Validate for VulnerabilityReferences {
    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_list("inner", &self.0, |reference| {
                reference.validate_version(version)
            })
            .into()
    }
}

#[cfg(test)]
mod test {
    use crate::{
        external_models::{normalized_string::NormalizedString, uri::Uri},
        models::vulnerability_source::VulnerabilitySource,
        validation,
    };

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

    #[test]
    fn valid_vulnerability_references_should_pass_validation() {
        let validation_result = VulnerabilityReferences(vec![VulnerabilityReference {
            id: NormalizedString::new("id"),
            vulnerability_source: VulnerabilitySource {
                name: Some(NormalizedString::new("name")),
                url: Some(Uri("https://www.example.com".to_string())),
            },
        }])
        .validate();

        assert!(validation_result.passed());
    }

    #[test]
    fn invalid_vulnerability_references_should_fail_validation() {
        let validation_result = VulnerabilityReferences(vec![VulnerabilityReference {
            id: NormalizedString("invalid\tid".to_string()),
            vulnerability_source: VulnerabilitySource {
                name: Some(NormalizedString("invalid\tname".to_string())),
                url: Some(Uri("invalid url".to_string())),
            },
        }])
        .validate();

        assert_eq!(
            validation_result,
            validation::list(
                "inner",
                [(
                    0,
                    vec![
                        validation::field(
                            "id",
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                        ),
                        validation::r#struct(
                            "vulnerability_source",
                            vec![
                                validation::field(
                                    "name",
                                    "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                ),
                                validation::field(
                                    "url",
                                    "Uri does not conform to RFC 3986"
                                )
                            ]
                        ),
                    ]
                )]
            )
        );
    }
}
