/*
 * 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::validate_date_time;
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, ValidationResult};

use super::attachment::Attachment;
use super::bom::SpecVersion;

/// 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 workaround: Option<String>,
    pub proof_of_concept: Option<VulnerabilityProofOfConcept>,
    pub advisories: Option<Advisories>,
    pub created: Option<DateTime>,
    pub published: Option<DateTime>,
    pub updated: Option<DateTime>,
    pub rejected: 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,
            workaround: None,
            proof_of_concept: None,
            advisories: None,
            created: None,
            published: None,
            updated: None,
            rejected: None,
            vulnerability_credits: None,
            tools: None,
            vulnerability_analysis: None,
            vulnerability_targets: None,
            properties: None,
        }
    }
}

impl Validate for Vulnerability {
    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_field_option("id", self.id.as_ref(), validate_normalized_string)
            .add_struct_option(
                "vulnerability_source",
                self.vulnerability_source.as_ref(),
                version,
            )
            .add_list(
                "vulnerability_references",
                self.vulnerability_references.as_ref(),
                |references| references.validate_version(version),
            )
            .add_list(
                "vulnerability_ratings",
                self.vulnerability_ratings.as_ref(),
                |ratings| ratings.validate_version(version),
            )
            .add_list("advisories", self.advisories.as_ref(), |advisories| {
                advisories.validate_version(version)
            })
            .add_field_option("created", self.created.as_ref(), validate_date_time)
            .add_field_option("published", self.published.as_ref(), validate_date_time)
            .add_field_option("updated", self.updated.as_ref(), validate_date_time)
            .add_struct_option(
                "vulnerability_credits",
                self.vulnerability_credits.as_ref(),
                version,
            )
            .add_struct_option("tools", self.tools.as_ref(), version)
            .add_struct_option(
                "vulnerability_analysis",
                self.vulnerability_analysis.as_ref(),
                version,
            )
            .add_struct_option(
                "vulnerability_targets",
                self.vulnerability_targets.as_ref(),
                version,
            )
            .add_struct_option("properties", self.properties.as_ref(), version)
            .into()
    }
}

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

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityProofOfConcept {
    pub reproduction_steps: Option<String>,
    pub environment: Option<String>,
    pub supporting_material: Option<Vec<Attachment>>,
}

#[cfg(test)]
mod test {
    use super::*;
    use pretty_assertions::assert_eq;

    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,
    };

    #[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()),
            workaround: Some("workaround".to_string()),
            proof_of_concept: Some(VulnerabilityProofOfConcept {
                reproduction_steps: Some("reproduction steps".to_string()),
                environment: Some("production".to_string()),
                supporting_material: Some(vec![Attachment {
                    content: "abcdefgh".to_string(),
                    content_type: Some("image/jpeg".to_string()),
                    encoding: Some("base64".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())),
            rejected: Some(DateTime("1969-06-28T01:20:00.00-04:00".to_string())),
            vulnerability_credits: Some(VulnerabilityCredits {
                organizations: Some(vec![OrganizationalEntity::new("name")]),
                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()),
                first_issued: Some(DateTime("2024-01-02T01:20:00.00-04:00".to_string())),
                last_updated: Some(DateTime("2024-01-10T01:20:00.00-04:00".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_version(SpecVersion::default());

        assert!(validation_result.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::Unknown("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()),
            workaround: Some("workaround".to_string()),
            proof_of_concept: Some(VulnerabilityProofOfConcept {
                reproduction_steps: Some("reproduction steps".to_string()),
                environment: Some("production".to_string()),
                supporting_material: Some(vec![Attachment {
                    content: "abcdefgh".to_string(),
                    content_type: Some("image/jpeg".to_string()),
                    encoding: Some("base64".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())),
            rejected: Some(DateTime("1970-01-01".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()),
                first_issued: Some(DateTime("invalid".to_string())),
                last_updated: Some(DateTime("invalid".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,
            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"
                                )
                            ]
                        ),
                        validation::list(
                            "vulnerability_references",
                            [(
                                0,
                                validation::list(
                                    "inner",
                                    [(
                                        0,
                                        validation::field(
                                            "id",
                                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                        )
                                    )]
                                )
                            )]
                        ),
                        validation::list(
                            "vulnerability_ratings",
                            [(
                                0,
                                validation::list(
                                    "inner",
                                    [(
                                        0,
                                        vec![
                                            validation::r#enum("severity", "Undefined severity"),
                                            validation::field(
                                                "score_method",
                                                "Unknown score method 'other method'",
                                            ),
                                            validation::field(
                                                "vector",
                                                "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                            )
                                        ]
                                    )]
                                )
                            )]
                        ),
                        validation::list(
                            "advisories",
                            [(
                                0,
                                validation::list(
                                    "inner",
                                    [(
                                        0,
                                        vec![
                                            validation::field(
                                                "title",
                                                "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                            ),
                                            validation::field(
                                                "url",
                                                "Uri does not conform to RFC 3986"
                                            )
                                        ]
                                    )]
                                )
                            )]
                        ),
                        validation::field("created", "DateTime does not conform to ISO 8601"),
                        validation::field("published", "DateTime does not conform to ISO 8601"),
                        validation::field("updated", "DateTime does not conform to ISO 8601"),
                        validation::r#struct(
                            "vulnerability_analysis",
                            vec![
                                validation::r#enum("state", "Undefined impact analysis state"),
                                validation::r#enum("justification", "Undefined impact analysis justification"),
                                validation::list(
                                    "responses",
                                    [(
                                        0,
                                        validation::custom("", ["Undefined response"])
                                    )]
                                ),
                                validation::field("first_issued", "DateTime does not conform to ISO 8601"),
                                validation::field("last_updated", "DateTime does not conform to ISO 8601"),
                            ]
                        ),
                        validation::r#struct(
                            "properties",
                            validation::list(
                                "inner",
                                [(
                                    0,
                                    validation::field(
                                        "value",
                                        "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                                    )
                                )]
                            )
                        )
                    ]
                )]
            )
        );
    }
}
