/*
 * 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::validate_date_time,
    prelude::DateTime,
    validation::{Validate, ValidationContext, ValidationError, ValidationResult},
};

use super::bom::SpecVersion;

/// Represents a vulnerability's analysis 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_vulnerabilityType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityAnalysis {
    pub state: Option<ImpactAnalysisState>,
    pub justification: Option<ImpactAnalysisJustification>,
    pub responses: Option<Vec<ImpactAnalysisResponse>>,
    pub detail: Option<String>,
    /// Added in spec version 1.5
    pub first_issued: Option<DateTime>,
    /// Added in spec version 1.5
    pub last_updated: Option<DateTime>,
}

impl VulnerabilityAnalysis {
    /// Construct a `VulnerabilityAnalysis`
    /// ```
    /// use cyclonedx_bom::models::vulnerability_analysis::{ImpactAnalysisJustification, ImpactAnalysisResponse, ImpactAnalysisState, VulnerabilityAnalysis};
    ///
    /// let analysis = VulnerabilityAnalysis::new(
    ///     Some(ImpactAnalysisState::NotAffected),
    ///     Some(ImpactAnalysisJustification::CodeNotReachable),
    ///     Some(vec![ImpactAnalysisResponse::Update])
    /// );
    /// ```
    pub fn new(
        state: Option<ImpactAnalysisState>,
        justification: Option<ImpactAnalysisJustification>,
        responses: Option<Vec<ImpactAnalysisResponse>>,
    ) -> Self {
        Self {
            state,
            justification,
            responses,
            detail: None,
            first_issued: None,
            last_updated: None,
        }
    }
}

impl Validate for VulnerabilityAnalysis {
    fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_enum_option("state", self.state.as_ref(), validate_impact_analysis_state)
            .add_enum_option(
                "justification",
                self.justification.as_ref(),
                validate_impact_analysis_justification,
            )
            .add_list_option("responses", self.responses.as_ref(), |response| {
                validate_impact_analysis_response(response)
            })
            .add_field_option(
                "first_issued",
                self.first_issued.as_ref(),
                validate_date_time,
            )
            .add_field_option(
                "last_updated",
                self.first_issued.as_ref(),
                validate_date_time,
            )
            .into()
    }
}

pub fn validate_impact_analysis_state(state: &ImpactAnalysisState) -> Result<(), ValidationError> {
    if matches!(state, ImpactAnalysisState::UndefinedImpactAnalysisState(_)) {
        return Err(ValidationError::new("Undefined impact analysis state"));
    }
    Ok(())
}

/// Specifies a vulnerability's state according to impact analysis.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_impactAnalysisStateType)
#[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum ImpactAnalysisState {
    Resolved,
    ResolvedWithPedigree,
    Exploitable,
    InTriage,
    FalsePositive,
    NotAffected,
    #[doc(hidden)]
    #[strum(default)]
    UndefinedImpactAnalysisState(String),
}

impl ImpactAnalysisState {
    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "resolved" => Self::Resolved,
            "resolved_with_pedigree" => Self::ResolvedWithPedigree,
            "exploitable" => Self::Exploitable,
            "in_triage" => Self::InTriage,
            "false_positive" => Self::FalsePositive,
            "not_affected" => Self::NotAffected,
            undefined => Self::UndefinedImpactAnalysisState(undefined.to_string()),
        }
    }
}

pub fn validate_impact_analysis_justification(
    justification: &ImpactAnalysisJustification,
) -> Result<(), ValidationError> {
    if matches!(
        justification,
        ImpactAnalysisJustification::UndefinedImpactAnalysisJustification(_)
    ) {
        return Err("Undefined impact analysis justification".into());
    }
    Ok(())
}

/// Justifies the vulnerability's state according to impact analysis.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_impactAnalysisJustificationType)
#[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum ImpactAnalysisJustification {
    CodeNotPresent,
    CodeNotReachable,
    RequiresConfiguration,
    RequiresDependency,
    RequiresEnvironment,
    ProtectedByCompiler,
    ProtectedAtRuntime,
    ProtectedAtPerimeter,
    ProtectedByMitigatingControl,
    #[doc(hidden)]
    #[strum(default)]
    UndefinedImpactAnalysisJustification(String),
}

impl ImpactAnalysisJustification {
    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "code_not_present" => Self::CodeNotPresent,
            "code_not_reachable" => Self::CodeNotReachable,
            "requires_configuration" => Self::RequiresConfiguration,
            "requires_dependency" => Self::RequiresDependency,
            "requires_environment" => Self::RequiresEnvironment,
            "protected_by_compiler" => Self::ProtectedByCompiler,
            "protected_at_runtime" => Self::ProtectedAtRuntime,
            "protected_at_perimeter" => Self::ProtectedAtPerimeter,
            "protected_by_mitigating_control" => Self::ProtectedByMitigatingControl,
            undefined => Self::UndefinedImpactAnalysisJustification(undefined.to_string()),
        }
    }
}

pub fn validate_impact_analysis_response(
    response: &ImpactAnalysisResponse,
) -> Result<(), ValidationError> {
    if matches!(response, ImpactAnalysisResponse::UndefinedResponse(_)) {
        return Err("Undefined response".into());
    }
    Ok(())
}

/// Provides a response to the vulnerability according to impact analysis.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_impactAnalysisResponsesType)
#[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum ImpactAnalysisResponse {
    CanNotFix,
    WillNotFix,
    Update,
    Rollback,
    WorkaroundAvailable,
    #[doc(hidden)]
    #[strum(default)]
    UndefinedResponse(String),
}

impl ImpactAnalysisResponse {
    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "can_not_fix" => Self::CanNotFix,
            "will_not_fix" => Self::WillNotFix,
            "update" => Self::Update,
            "rollback" => Self::Rollback,
            "workaround_available" => Self::WorkaroundAvailable,
            undefined => Self::UndefinedResponse(undefined.to_string()),
        }
    }
}

#[cfg(test)]
mod test {
    use crate::validation;

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

    #[test]
    fn valid_vulnerability_analysis_should_pass_validation() {
        let validation_result = VulnerabilityAnalysis {
            state: Some(ImpactAnalysisState::NotAffected),
            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())),
        }
        .validate();

        assert!(validation_result.passed());
    }

    #[test]
    fn invalid_vulnerability_analysis_should_fail_validation() {
        let validation_result = 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())),
        }
        .validate();

        assert_eq!(
            validation_result,
            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")
            ]
            .into()
        );
    }
}
