/*
 * 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 once_cell::sync::Lazy;
use regex::Regex;

use crate::external_models::normalized_string::NormalizedString;
use crate::validation::{Validate, ValidationContext, ValidationError, ValidationResult};

use super::bom::SpecVersion;

/// Defines how a component or service is affected by 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_vulnerabilityType)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VulnerabilityTarget {
    pub bom_ref: String,
    pub versions: Option<Versions>,
}

impl VulnerabilityTarget {
    /// Construct a `VulnerabilityTarget` be referring to a component or service via a BOM reference
    /// ```
    /// use cyclonedx_bom::models::vulnerability_target::VulnerabilityTarget;
    ///
    /// let target = VulnerabilityTarget::new("12a34a5b-6780-1bae-2345-67890cfe12a3".to_string());
    /// ```
    pub fn new(bom_ref: String) -> Self {
        Self {
            bom_ref,
            versions: None,
        }
    }
}

impl Validate for VulnerabilityTarget {
    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_struct_option("versions", self.versions.as_ref(), version)
            .into()
    }
}

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

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

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

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Version {
    pub version_range: VersionRange,
    pub status: Status,
}

impl Version {
    /// Construct a `Version by providing a version and a status`
    /// ```
    /// use cyclonedx_bom::models::vulnerability_target::Version;
    ///
    /// let version = Version::new("1.0", "unaffected");
    /// ```
    pub fn new(version: &str, status: &str) -> Self {
        Version {
            version_range: VersionRange::new(version),
            status: Status::new_unchecked(status),
        }
    }
}

impl Validate for Version {
    fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
        ValidationContext::new()
            .add_enum("version_range", &self.version_range, validate_version_range)
            .add_enum("status", &self.status, validate_status)
            .into()
    }
}

pub fn validate_version_range(range: &VersionRange) -> Result<(), ValidationError> {
    if matches!(range, VersionRange::UndefinedVersionRange(_)) {
        return Err(ValidationError::new("Undefined version range"));
    }
    Ok(())
}

/// Specifies a single version or a version range.
///
/// Defined via the [PURL specification](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst)
/// Spec for version ranges still work in progress [PURL version-range-spec](https://github.com/package-url/purl-spec/blob/version-range-spec/VERSION-RANGE-SPEC.rst)
#[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
pub enum VersionRange {
    #[strum(default)]
    Version(NormalizedString),
    #[strum(default)]
    Range(NormalizedString),
    #[doc(hidden)]
    #[strum(default)]
    UndefinedVersionRange(String),
}

impl VersionRange {
    pub fn new(value: &str) -> Self {
        match matches_purl_version_range_regex(value) {
            true => VersionRange::Range(NormalizedString::new(value)),
            false => VersionRange::Version(NormalizedString::new(value)),
        }
    }
}

fn matches_purl_version_range_regex(value: &str) -> bool {
    static PURL_VERSION_RANGE_REGEX: Lazy<Regex> =
        Lazy::new(|| Regex::new(r"^vers:.*$").expect("Failed to compile regex."));

    PURL_VERSION_RANGE_REGEX.is_match(value)
}

pub fn validate_status(status: &Status) -> Result<(), ValidationError> {
    if matches!(status, Status::UndefinedStatus(_)) {
        return Err(ValidationError::new("Undefined status"));
    }
    Ok(())
}

/// Specifies if a vulnerability affects a component or service.
///
/// Defined via the [XML schema](https://cyclonedx.org/docs/1.4/xml/#type_impactAnalysisAffectedStatusType)
#[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
#[strum(serialize_all = "kebab-case")]
pub enum Status {
    Affected,
    Unaffected,
    Unknown,
    #[doc(hidden)]
    #[strum(default)]
    UndefinedStatus(String),
}

impl Status {
    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
        match value.as_ref() {
            "affected" => Self::Affected,
            "unaffected" => Self::Unaffected,
            "unknown" => Self::Unknown,
            undefined => Self::UndefinedStatus(undefined.to_string()),
        }
    }
}

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

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

    #[test]
    fn valid_version_range() {
        assert_eq!(
            VersionRange::Version(NormalizedString("1.0".to_string())),
            Version::new("1.0", "unaffected").version_range,
        );
    }

    #[test]
    fn valid_vulnerability_targets_should_pass_validation() {
        let validation_result = VulnerabilityTargets(vec![VulnerabilityTarget {
            bom_ref: "bom ref".to_string(),
            versions: Some(Versions(vec![Version {
                version_range: VersionRange::Version(NormalizedString::new("1.0")),
                status: Status::Affected,
            }])),
        }])
        .validate();

        assert!(validation_result.passed());
    }

    #[test]
    fn invalid_vulnerability_targets_should_fail_validation() {
        let validation_result = VulnerabilityTargets(vec![VulnerabilityTarget {
            bom_ref: "bom ref".to_string(),
            versions: Some(Versions(vec![Version {
                version_range: VersionRange::UndefinedVersionRange("invalid\tversion".to_string()),
                status: Status::UndefinedStatus("invalid\tstatus".to_string()),
            }])),
        }])
        .validate();

        assert_eq!(
            validation_result,
            validation::list(
                "inner",
                [(
                    0,
                    validation::r#struct(
                        "versions",
                        validation::list(
                            "inner",
                            [(
                                0,
                                vec![
                                    validation::r#enum("version_range", "Undefined version range"),
                                    validation::r#enum("status", "Undefined status"),
                                ]
                            )]
                        )
                    )
                )]
            )
        );
    }
}
