/*
 * 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::{date_time::DateTime, normalized_string::NormalizedString};
use crate::models::advisory::Advisories;
use crate::models::property::Properties;
use crate::models::tool::Tools;
use crate::models::vulnerability_analysis::VulnerabilityAnalysis;
use crate::models::vulnerability_credits::VulnerabilityCredits;
use crate::models::vulnerability_rating::VulnerabilityRatings;
use crate::models::vulnerability_reference::VulnerabilityReferences;
use crate::models::vulnerability_source::VulnerabilitySource;
use crate::models::vulnerability_target::VulnerabilityTargets;
use crate::validation::{Validate, ValidationContext, ValidationPathComponent, ValidationResult};

/// Represents a vulnerability as described in the [CycloneDX use cases](https://cyclonedx.org/use-cases/#vulnerability-exploitability)
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_vulnerabilitiesType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vulnerability {
    pub bom_ref: Option<String>,
    pub id: Option<NormalizedString>,
    pub vulnerability_source: Option<VulnerabilitySource>,
    pub vulnerability_references: Option<VulnerabilityReferences>,
    pub vulnerability_ratings: Option<VulnerabilityRatings>,
    pub cwes: Option<Vec<u32>>,
    pub description: Option<String>,
    pub detail: Option<String>,
    pub recommendation: Option<String>,
    pub advisories: Option<Advisories>,
    pub created: Option<DateTime>,
    pub published: Option<DateTime>,
    pub updated: Option<DateTime>,
    pub vulnerability_credits: Option<VulnerabilityCredits>,
    pub tools: Option<Tools>,
    pub vulnerability_analysis: Option<VulnerabilityAnalysis>,
    pub vulnerability_targets: Option<VulnerabilityTargets>,
    pub properties: Option<Properties>,
}

impl Vulnerability {
    /// Construct a `Vulnerability` with a BOM reference
    /// ```
    /// use cyclonedx_bom::models::vulnerability::Vulnerability;
    ///
    /// let vulnerability = Vulnerability::new(Some("12a34a5b-6780-1bae-2345-67890cfe12a3".to_string()));
    /// ```
    pub fn new(bom_ref: Option<String>) -> Self {
        Self {
            bom_ref,
            id: None,
            vulnerability_source: None,
            vulnerability_references: None,
            vulnerability_ratings: None,
            cwes: None,
            description: None,
            detail: None,
            recommendation: None,
            advisories: None,
            created: None,
            published: None,
            updated: None,
            vulnerability_credits: None,
            tools: None,
            vulnerability_analysis: None,
            vulnerability_targets: None,
            properties: None,
        }
    }
}

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

        if let Some(id) = &self.id {
            let context = context.with_struct("Vulnerability", "id");

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

        if let Some(vulnerability_source) = &self.vulnerability_source {
            let context = context.with_struct("Vulnerability", "vulnerability_source");

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

        if let Some(vulnerability_references) = &self.vulnerability_references {
            let context = context.with_struct("Vulnerability", "vulnerability_references");

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

        if let Some(vulnerability_ratings) = &self.vulnerability_ratings {
            let context = context.with_struct("Vulnerability", "vulnerability_ratings");

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

        if let Some(advisories) = &self.advisories {
            let context = context.with_struct("Vulnerability", "advisories");

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

        if let Some(created) = &self.created {
            let context = context.with_struct("Vulnerability", "created");

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

        if let Some(published) = &self.published {
            let context = context.with_struct("Vulnerability", "published");

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

        if let Some(updated) = &self.updated {
            let context = context.with_struct("Vulnerability", "updated");

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

        if let Some(vulnerability_credits) = &self.vulnerability_credits {
            let context = context.with_struct("Vulnerability", "vulnerability_credits");

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

        if let Some(tools) = &self.tools {
            let context = context.with_struct("Vulnerability", "tools");

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

        if let Some(vulnerability_analysis) = &self.vulnerability_analysis {
            let context = context.with_struct("Vulnerability", "vulnerability_analysis");

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

        if let Some(vulnerability_targets) = &self.vulnerability_targets {
            let context = context.with_struct("Vulnerability", "vulnerability_targets");

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

        if let Some(properties) = &self.properties {
            let context = context.with_struct("Vulnerability", "properties");

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

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

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

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

        for (index, vulnerability) in self.0.iter().enumerate() {
            let context = context.extend_context(vec![ValidationPathComponent::Array { index }]);
            results.push(vulnerability.validate_with_context(context));
        }

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

#[cfg(test)]
mod test {
    use super::*;
    use crate::{
        external_models::uri::Uri,
        models::{
            advisory::Advisory,
            organization::OrganizationalEntity,
            property::Property,
            vulnerability_analysis::{
                ImpactAnalysisJustification, ImpactAnalysisResponse, ImpactAnalysisState,
            },
            vulnerability_rating::{Score, ScoreMethod, Severity, VulnerabilityRating},
            vulnerability_reference::VulnerabilityReference,
            vulnerability_target::{Status, Version, VersionRange, Versions, VulnerabilityTarget},
        },
        validation::FailureReason,
    };

    #[test]
    fn valid_vulnerabilities_should_pass_validation() {
        let validation_result = Vulnerabilities(vec![Vulnerability {
            bom_ref: Some("bom ref".to_string()),
            id: Some(NormalizedString::new("id")),
            vulnerability_source: Some(VulnerabilitySource {
                name: Some(NormalizedString::new("name")),
                url: Some(Uri("https://example.com".to_string())),
            }),
            vulnerability_references: Some(VulnerabilityReferences(vec![VulnerabilityReference {
                id: NormalizedString::new("id"),
                vulnerability_source: VulnerabilitySource {
                    name: Some(NormalizedString::new("name")),
                    url: Some(Uri("https://example.com".to_string())),
                },
            }])),
            vulnerability_ratings: Some(VulnerabilityRatings(vec![VulnerabilityRating {
                vulnerability_source: Some(VulnerabilitySource {
                    name: Some(NormalizedString::new("name")),
                    url: Some(Uri("https://example.com".to_string())),
                }),
                score: Some(10.0.into()),
                severity: Some(Severity::Critical),
                score_method: Some(ScoreMethod::CVSSv3),
                vector: Some(NormalizedString::new("vector")),
                justification: Some("justification".to_string()),
            }])),
            cwes: Some(vec![1, 2, 3]),
            description: Some("description".to_string()),
            detail: Some("detail".to_string()),
            recommendation: Some("recommendation".to_string()),
            advisories: Some(Advisories(vec![Advisory {
                title: Some(NormalizedString::new("title")),
                url: Uri("https://example.com".to_string()),
            }])),
            created: Some(DateTime("1969-06-28T01:20:00.00-04:00".to_string())),
            published: Some(DateTime("1969-06-28T01:20:00.00-04:00".to_string())),
            updated: Some(DateTime("1969-06-28T01:20:00.00-04:00".to_string())),
            vulnerability_credits: Some(VulnerabilityCredits {
                organizations: Some(vec![OrganizationalEntity {
                    name: Some(NormalizedString::new("name")),
                    url: None,
                    contact: None,
                }]),
                individuals: None,
            }),
            tools: None,
            vulnerability_analysis: Some(VulnerabilityAnalysis {
                state: Some(ImpactAnalysisState::Exploitable),
                justification: Some(ImpactAnalysisJustification::CodeNotReachable),
                responses: Some(vec![ImpactAnalysisResponse::Update]),
                detail: Some("detail".to_string()),
            }),
            vulnerability_targets: Some(VulnerabilityTargets(vec![VulnerabilityTarget {
                bom_ref: "bom ref".to_string(),
                versions: Some(Versions(vec![Version {
                    version_range: VersionRange::Version(NormalizedString::new("version")),
                    status: Status::Affected,
                }])),
            }])),
            properties: Some(Properties(vec![Property {
                name: "name".to_string(),
                value: NormalizedString::new("value"),
            }])),
        }])
        .validate();

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

    #[test]
    fn invalid_vulnerabilities_should_fail_validation() {
        let validation_result = Vulnerabilities(vec![Vulnerability {
            bom_ref: Some("bom ref".to_string()),
            id: Some(NormalizedString("invalid\tid".to_string())),
            vulnerability_source: Some(VulnerabilitySource {
                name: Some(NormalizedString("invalid\tname".to_string())),
                url: Some(Uri("invalid url".to_string())),
            }),
            vulnerability_references: Some(VulnerabilityReferences(vec![VulnerabilityReference {
                id: NormalizedString("invalid\tid".to_string()),
                vulnerability_source: VulnerabilitySource {
                    name: Some(NormalizedString::new("name")),
                    url: Some(Uri("https://example.com".to_string())),
                },
            }])),
            vulnerability_ratings: Some(VulnerabilityRatings(vec![VulnerabilityRating {
                vulnerability_source: Some(VulnerabilitySource {
                    name: Some(NormalizedString::new("name")),
                    url: Some(Uri("https://example.com".to_string())),
                }),
                score: Score::from_f32(10.0),
                severity: Some(Severity::UndefinedSeverity("undefined".to_string())),
                score_method: Some(ScoreMethod::Other("other method".to_string())),
                vector: Some(NormalizedString("invalid\tvector".to_string())),
                justification: Some("justification".to_string()),
            }])),
            cwes: Some(vec![1, 2, 3]),
            description: Some("description".to_string()),
            detail: Some("detail".to_string()),
            recommendation: Some("recommendation".to_string()),
            advisories: Some(Advisories(vec![Advisory {
                title: Some(NormalizedString("invalid\ttitle".to_string())),
                url: Uri("invalid url".to_string()),
            }])),
            created: Some(DateTime("Thursday".to_string())),
            published: Some(DateTime("1970-01-01".to_string())),
            updated: Some(DateTime("invalid date".to_string())),
            vulnerability_credits: None,
            tools: None,
            vulnerability_analysis: Some(VulnerabilityAnalysis {
                state: Some(ImpactAnalysisState::UndefinedImpactAnalysisState(
                    "undefined".to_string(),
                )),
                justification: Some(
                    ImpactAnalysisJustification::UndefinedImpactAnalysisJustification(
                        "undefined".to_string(),
                    ),
                ),
                responses: Some(vec![ImpactAnalysisResponse::UndefinedResponse(
                    "undefined".to_string(),
                )]),
                detail: Some("detail".to_string()),
            }),
            vulnerability_targets: None,
            properties: Some(Properties(vec![Property {
                name: "name".to_string(),
                value: NormalizedString("invalid\tvalue".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::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "id".to_string()
                            },
                        ])
                    },
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_source".to_string()
                            },
                            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::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_source".to_string()
                            },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilitySource".to_string(),
                                field_name: "url".to_string()
                            },
                        ])
                    },
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_references".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityReference".to_string(),
                                field_name: "id".to_string()
                            },
                        ])
                    },
                    FailureReason {
                        message: "Undefined severity".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_ratings".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityRating".to_string(),
                                field_name: "severity".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_ratings".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityRating".to_string(),
                                field_name: "vector".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "advisories".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Advisory".to_string(),
                                field_name: "title".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "Uri does not conform to RFC 3986".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "advisories".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Advisory".to_string(),
                                field_name: "url".to_string()
                            },
                        ])
                    },
                    FailureReason {
                        message: "DateTime does not conform to ISO 8601".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "created".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "DateTime does not conform to ISO 8601".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "published".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "DateTime does not conform to ISO 8601".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "updated".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "Undefined impact analysis state".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_analysis".to_string()
                            },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityAnalysis".to_string(),
                                field_name: "state".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "Undefined impact analysis justification".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_analysis".to_string()
                            },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityAnalysis".to_string(),
                                field_name: "justification".to_string()
                            }
                        ])
                    },
                    FailureReason {
                        message: "Undefined response".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "vulnerability_analysis".to_string()
                            },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityAnalysis".to_string(),
                                field_name: "responses".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                        ])
                    },
                    FailureReason {
                        message:
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                .to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Vulnerability".to_string(),
                                field_name: "properties".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                            ValidationPathComponent::Struct {
                                struct_name: "Property".to_string(),
                                field_name: "value".to_string()
                            }
                        ])
                    },
                ]
            }
        );
    }
}
