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

use super::bom::SpecVersion;

/// 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_version(&self, version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_struct_option(
                "vulnerability_source",
                self.vulnerability_source.as_ref(),
                version,
            )
            .add_enum_option("severity", self.severity.as_ref(), validate_severity)
            .add_field_option("score_method", self.score_method.as_ref(), |sm| {
                validate_score_method(sm, version)
            })
            .add_field_option("vector", self.vector.as_ref(), validate_normalized_string)
            .into()
    }
}

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

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

/// 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
    }
}

pub fn validate_severity(severity: &Severity) -> Result<(), ValidationError> {
    if matches!(severity, Severity::UndefinedSeverity(_)) {
        return Err("Undefined severity".into());
    }
    Ok(())
}

/// 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, strum::Display)]
#[strum(serialize_all = "kebab-case")]
pub enum Severity {
    Critical,
    High,
    Medium,
    Low,
    Info,
    None,
    Unknown,
    #[doc(hidden)]
    #[strum(default)]
    UndefinedSeverity(String),
}

impl Severity {
    pub 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()),
        }
    }
}

pub fn validate_score_method(
    method: &ScoreMethod,
    version: SpecVersion,
) -> Result<(), ValidationError> {
    if version <= SpecVersion::V1_4 {
        if ScoreMethod::OWASP < *method {
            return Err(format!("Unknown score method '{method}'").into());
        }
    } else if version <= SpecVersion::V1_5 {
        if let ScoreMethod::Unknown(unknown) = method {
            return Err(format!("Unknown score method '{unknown}'").into());
        }
    }
    Ok(())
}

/// 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, PartialOrd, strum::Display)]
#[repr(u16)]
pub enum ScoreMethod {
    CVSSv2 = 1,
    CVSSv3 = 2,
    CVSSv31 = 3,
    OWASP = 4,
    /// Added in version 1.5
    CVSSv4 = 5,
    /// Added in version 1.5
    SSVC = 6,
    #[strum(default)]
    Unknown(String),
}

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

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

    use pretty_assertions::assert_eq;

    #[test]
    fn valid_vulnerability_score_method() {
        assert!(
            validate_score_method(&ScoreMethod::new_unchecked("OWASP"), SpecVersion::V1_4).is_ok()
        );
        assert!(
            validate_score_method(&ScoreMethod::new_unchecked("OWASP"), SpecVersion::V1_5).is_ok()
        );
        assert!(
            validate_score_method(&ScoreMethod::new_unchecked("SSVC"), SpecVersion::V1_4).is_err()
        );
        assert!(
            validate_score_method(&ScoreMethod::new_unchecked("SSVC"), SpecVersion::V1_5).is_ok()
        );
    }

    #[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!(validation_result.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,
            validation::list(
                "inner",
                [(
                    0,
                    vec![
                        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::r#enum("severity", "Undefined severity"),
                        validation::field(
                            "vector",
                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
                        )
                    ],
                )]
            )
        );
    }
}
