/*
 * 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 ordered_float::OrderedFloat;

use crate::external_models::normalized_string::NormalizedString;
use crate::models::vulnerability_source::VulnerabilitySource;
use crate::validation::{
    FailureReason, Validate, ValidationContext, ValidationPathComponent, ValidationResult,
};

/// Represents a vulnerability's rating 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_ratingType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityRating {
    pub vulnerability_source: Option<VulnerabilitySource>,
    pub score: Option<Score>,
    pub severity: Option<Severity>,
    pub score_method: Option<ScoreMethod>,
    pub vector: Option<NormalizedString>,
    pub justification: Option<String>,
}

impl VulnerabilityRating {
    /// Construct a `VulnerabilityRating` with a score, severity and scoring method.
    /// ```
    /// use cyclonedx_bom::models::vulnerability_rating::{Score, ScoreMethod, Severity, VulnerabilityRating};
    ///
    /// let rating = VulnerabilityRating::new(Score::from_f32(9.8), Some(Severity::Critical), Some(ScoreMethod::CVSSv3));
    /// ```
    pub fn new(
        score: Option<Score>,
        severity: Option<Severity>,
        score_method: Option<ScoreMethod>,
    ) -> Self {
        Self {
            vulnerability_source: None,
            score,
            severity,
            score_method,
            vector: None,
            justification: None,
        }
    }
}

// todo: how to decide what to validate, check this
impl Validate for VulnerabilityRating {
    fn validate_with_context(&self, context: ValidationContext) -> ValidationResult {
        let mut results: Vec<ValidationResult> = vec![];

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

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

        if let Some(severity) = &self.severity {
            let context = context.with_struct("VulnerabilityRating", "severity");

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

        if let Some(vector) = &self.vector {
            let context = context.with_struct("VulnerabilityRating", "vector");

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

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

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

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

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

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

/// Defines a vulnerability's score. According to the spec this is a decimal value.
/// However, deriving `Eq` on `Bom` and all its subsequent data structures
/// prevents us from using a float value for `Score`. Implementing `Score` as
/// an i32 allows us to easily derive `Eq`. We have chosen a factor of 1000 to
/// convert a f32 into i32 because OWASP's scoring method uses up to three decimal places.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_ratingType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Score(OrderedFloat<f32>);

impl Score {
    pub fn new_unchecked(score: f32) -> Self {
        Score(score.into())
    }

    pub fn from_f32(score: f32) -> Option<Self> {
        Some(Score(score.into()))
    }

    pub fn to_f32(&self) -> f32 {
        self.0 .0
    }
}

impl From<f32> for Score {
    fn from(value: f32) -> Self {
        Score(value.into())
    }
}

impl From<Score> for f32 {
    fn from(value: Score) -> f32 {
        value.0 .0
    }
}

/// Specifies a vulnerability's severity adopted by the analysis method.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_severityType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Severity {
    Critical,
    High,
    Medium,
    Low,
    Info,
    None,
    Unknown,
    #[doc(hidden)]
    UndefinedSeverity(String),
}

impl Severity {
    pub(crate) fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "critical" => Self::Critical,
            "high" => Self::High,
            "medium" => Self::Medium,
            "low" => Self::Low,
            "info" => Self::Info,
            "none" => Self::None,
            "unknown" => Self::Unknown,
            undefined => Self::UndefinedSeverity(undefined.to_string()),
        }
    }
}

impl Validate for Severity {
    fn validate_with_context(&self, context: ValidationContext) -> ValidationResult {
        match self {
            Severity::UndefinedSeverity(_) => ValidationResult::Failed {
                reasons: vec![FailureReason {
                    message: "Undefined severity".to_string(),
                    context,
                }],
            },
            _ => ValidationResult::Passed,
        }
    }
}

impl ToString for Severity {
    fn to_string(&self) -> String {
        match self {
            Severity::Critical => "critical",
            Severity::High => "high",
            Severity::Medium => "medium",
            Severity::Low => "low",
            Severity::Info => "info",
            Severity::None => "none",
            Severity::Unknown => "unknown",
            Severity::UndefinedSeverity(undefined) => undefined,
        }
        .to_string()
    }
}

/// Specifies the risk scoring method or standard used.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_scoreSourceType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScoreMethod {
    CVSSv2,
    CVSSv3,
    CVSSv31,
    OWASP,
    Other(String),
}

impl ScoreMethod {
    pub(crate) fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "CVSSv2" => Self::CVSSv2,
            "CVSSv3" => Self::CVSSv3,
            "CVSSv31" => Self::CVSSv31,
            "OWASP" => Self::OWASP,
            score_method => Self::Other(score_method.to_string()),
        }
    }
}

impl ToString for ScoreMethod {
    fn to_string(&self) -> String {
        match self {
            ScoreMethod::CVSSv2 => "CVSSv2",
            ScoreMethod::CVSSv3 => "CVSSv3",
            ScoreMethod::CVSSv31 => "CVSSv31",
            ScoreMethod::OWASP => "OWASP",
            ScoreMethod::Other(score_method) => score_method,
        }
        .to_string()
    }
}

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

    use pretty_assertions::assert_eq;

    #[test]
    fn valid_vulnerability_ratings_should_pass_validation() {
        let validation_result = VulnerabilityRatings(vec![VulnerabilityRating {
            vulnerability_source: Some(VulnerabilitySource {
                name: Some(NormalizedString::new("name")),
                url: Some(Uri("https://example.com".to_string())),
            }),
            score: None,
            severity: Some(Severity::Info),
            score_method: None,
            vector: Some(NormalizedString::new("vector")),
            justification: None,
        }])
        .validate();

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

    #[test]
    fn invalid_vulnerability_ratings_should_fail_validation() {
        let validation_result = VulnerabilityRatings(vec![VulnerabilityRating {
            vulnerability_source: Some(VulnerabilitySource {
                name: Some(NormalizedString("invalid\tname".to_string())),
                url: Some(Uri("invalid url".to_string())),
            }),
            score: None,
            severity: Some(Severity::UndefinedSeverity("undefined".to_string())),
            score_method: None,
            vector: Some(NormalizedString("invalid\tvector".to_string())),
            justification: None,
        }])
        .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: "VulnerabilityRating".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: "VulnerabilityRating".to_string(),
                                field_name: "vulnerability_source".to_string()
                            },
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilitySource".to_string(),
                                field_name: "url".to_string()
                            },
                        ])
                    },
                    FailureReason {
                        message: "Undefined severity".to_string(),
                        context: ValidationContext(vec![
                            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: "VulnerabilityRating".to_string(),
                                field_name: "vector".to_string()
                            },
                        ])
                    },
                ]
            }
        );
    }
}
