/*
 * 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::validation::{
    FailureReason, Validate, ValidationContext, ValidationPathComponent, ValidationResult,
};

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

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

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

        if let Some(state) = &self.state {
            let context = context.with_struct("VulnerabilityAnalysis", "state");

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

        if let Some(justification) = &self.justification {
            let context = context.with_struct("VulnerabilityAnalysis", "justification");

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

        if let Some(responses) = &self.responses {
            for (index, response) in responses.iter().enumerate() {
                let context = context.extend_context(vec![
                    ValidationPathComponent::Struct {
                        struct_name: "VulnerabilityAnalysis".to_string(),
                        field_name: "responses".to_string(),
                    },
                    ValidationPathComponent::Array { index },
                ]);
                results.push(response.validate_with_context(context));
            }
        }

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

/// 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)]
pub enum ImpactAnalysisState {
    Resolved,
    ResolvedWithPedigree,
    Exploitable,
    InTriage,
    FalsePositive,
    NotAffected,
    #[doc(hidden)]
    UndefinedImpactAnalysisState(String),
}

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

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

impl ToString for ImpactAnalysisState {
    fn to_string(&self) -> String {
        match self {
            ImpactAnalysisState::Resolved => "resolved",
            ImpactAnalysisState::ResolvedWithPedigree => "resolved_with_pedigree",
            ImpactAnalysisState::Exploitable => "exploitable",
            ImpactAnalysisState::InTriage => "in_triage",
            ImpactAnalysisState::FalsePositive => "false_positive",
            ImpactAnalysisState::NotAffected => "not_affected",
            ImpactAnalysisState::UndefinedImpactAnalysisState(undefined) => undefined,
        }
        .to_string()
    }
}

/// 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)]
pub enum ImpactAnalysisJustification {
    CodeNotPresent,
    CodeNotReachable,
    RequiresConfiguration,
    RequiresDependency,
    RequiresEnvironment,
    ProtectedByCompiler,
    ProtectedAtRuntime,
    ProtectedAtPerimeter,
    ProtectedByMitigatingControl,
    #[doc(hidden)]
    UndefinedImpactAnalysisJustification(String),
}

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

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

impl ToString for ImpactAnalysisJustification {
    fn to_string(&self) -> String {
        match self {
            ImpactAnalysisJustification::CodeNotPresent => "code_not_present",
            ImpactAnalysisJustification::CodeNotReachable => "code_not_reachable",
            ImpactAnalysisJustification::RequiresConfiguration => "requires_configuration",
            ImpactAnalysisJustification::RequiresDependency => "requires_dependency",
            ImpactAnalysisJustification::RequiresEnvironment => "requires_environment",
            ImpactAnalysisJustification::ProtectedByCompiler => "protected_by_compiler",
            ImpactAnalysisJustification::ProtectedAtRuntime => "protected_at_runtime",
            ImpactAnalysisJustification::ProtectedAtPerimeter => "protected_at_perimeter",
            ImpactAnalysisJustification::ProtectedByMitigatingControl => {
                "protected_by_mitigating_control"
            }
            ImpactAnalysisJustification::UndefinedImpactAnalysisJustification(undefined) => {
                undefined
            }
        }
        .to_string()
    }
}

/// 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)]
pub enum ImpactAnalysisResponse {
    CanNotFix,
    WillNotFix,
    Update,
    Rollback,
    WorkaroundAvailable,
    #[doc(hidden)]
    UndefinedResponse(String),
}

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

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

impl ToString for ImpactAnalysisResponse {
    fn to_string(&self) -> String {
        match self {
            ImpactAnalysisResponse::CanNotFix => "can_not_fix",
            ImpactAnalysisResponse::WillNotFix => "will_not_fix",
            ImpactAnalysisResponse::Update => "update",
            ImpactAnalysisResponse::Rollback => "rollback",
            ImpactAnalysisResponse::WorkaroundAvailable => "workaround_available",
            ImpactAnalysisResponse::UndefinedResponse(undefined) => undefined,
        }
        .to_string()
    }
}

#[cfg(test)]
mod test {
    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()),
        }
        .validate();

        assert_eq!(validation_result, ValidationResult::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()),
        }
        .validate();

        assert_eq!(
            validation_result,
            ValidationResult::Failed {
                reasons: vec![
                    FailureReason {
                        message: "Undefined impact analysis state".to_string(),
                        context: ValidationContext(vec![ValidationPathComponent::Struct {
                            struct_name: "VulnerabilityAnalysis".to_string(),
                            field_name: "state".to_string()
                        },])
                    },
                    FailureReason {
                        message: "Undefined impact analysis justification".to_string(),
                        context: ValidationContext(vec![ValidationPathComponent::Struct {
                            struct_name: "VulnerabilityAnalysis".to_string(),
                            field_name: "justification".to_string()
                        },])
                    },
                    FailureReason {
                        message: "Undefined response".to_string(),
                        context: ValidationContext(vec![
                            ValidationPathComponent::Struct {
                                struct_name: "VulnerabilityAnalysis".to_string(),
                                field_name: "responses".to_string()
                            },
                            ValidationPathComponent::Array { index: 0 },
                        ])
                    },
                ]
            }
        );
    }
}
