/*
 * 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::{
    errors::XmlReadError,
    external_models::normalized_string::NormalizedString,
    models,
    specs::common::vulnerability_source::VulnerabilitySource,
    utilities::{convert_optional, convert_vec},
    xml::{
        read_lax_validation_list_tag, read_lax_validation_tag, read_simple_tag, to_xml_read_error,
        to_xml_write_error, unexpected_element_error, write_close_tag, write_simple_tag,
        write_start_tag, FromXml, FromXmlType, ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::{reader, writer::XmlEvent};

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(transparent)]
pub(crate) struct VulnerabilityRatings(Vec<VulnerabilityRating>);

impl From<models::vulnerability_rating::VulnerabilityRatings> for VulnerabilityRatings {
    fn from(other: models::vulnerability_rating::VulnerabilityRatings) -> Self {
        VulnerabilityRatings(convert_vec(other.0))
    }
}

impl From<VulnerabilityRatings> for models::vulnerability_rating::VulnerabilityRatings {
    fn from(other: VulnerabilityRatings) -> Self {
        models::vulnerability_rating::VulnerabilityRatings(convert_vec(other.0))
    }
}

const VULNERABILITY_RATINGS_TAG: &str = "ratings";

impl ToXml for VulnerabilityRatings {
    fn write_xml_element<W: std::io::Write>(
        &self,
        writer: &mut xml::EventWriter<W>,
    ) -> Result<(), crate::errors::XmlWriteError> {
        write_start_tag(writer, VULNERABILITY_RATINGS_TAG)?;

        for vulnerability_rating in &self.0 {
            vulnerability_rating.write_xml_element(writer)?;
        }

        write_close_tag(writer, VULNERABILITY_RATINGS_TAG)?;

        Ok(())
    }
}

impl FromXml for VulnerabilityRatings {
    fn read_xml_element<R: std::io::Read>(
        event_reader: &mut xml::EventReader<R>,
        element_name: &xml::name::OwnedName,
        _attributes: &[xml::attribute::OwnedAttribute],
    ) -> Result<Self, XmlReadError>
    where
        Self: Sized,
    {
        read_lax_validation_list_tag(event_reader, element_name, VULNERABILITY_RATING_TAG)
            .map(VulnerabilityRatings)
    }
}

// todo: check naming, in XML spec its ScoreSourceType, in JSON spec it's ScoreMethod
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct VulnerabilityRating {
    #[serde(rename = "source", skip_serializing_if = "Option::is_none")]
    vulnerability_source: Option<VulnerabilitySource>,
    #[serde(skip_serializing_if = "Option::is_none")]
    score: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    severity: Option<Severity>,
    #[serde(rename = "method", skip_serializing_if = "Option::is_none")]
    score_method: Option<ScoreMethod>,
    #[serde(skip_serializing_if = "Option::is_none")]
    vector: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    justification: Option<String>,
}

impl From<models::vulnerability_rating::VulnerabilityRating> for VulnerabilityRating {
    fn from(other: models::vulnerability_rating::VulnerabilityRating) -> Self {
        Self {
            vulnerability_source: convert_optional(other.vulnerability_source),
            score: other.score.map(|s| s.to_f32()),
            severity: convert_optional(other.severity),
            score_method: convert_optional(other.score_method),
            vector: other.vector.map(|v| v.to_string()),
            justification: other.justification,
        }
    }
}

impl From<VulnerabilityRating> for models::vulnerability_rating::VulnerabilityRating {
    fn from(other: VulnerabilityRating) -> Self {
        Self {
            vulnerability_source: convert_optional(other.vulnerability_source),
            score: convert_optional(other.score),
            severity: convert_optional(other.severity),
            score_method: convert_optional(other.score_method),
            vector: other.vector.map(NormalizedString::new_unchecked),
            justification: other.justification,
        }
    }
}

const VULNERABILITY_RATING_TAG: &str = "rating";
const VULNERABILITY_SOURCE_TAG: &str = "source";
const SCORE_TAG: &str = "score";
const SEVERITY_TAG: &str = "severity";
const SCORE_METHOD_TAG: &str = "method";
const VECTOR_TAG: &str = "vector";
const JUSTIFICATION_TAG: &str = "justification";

impl ToXml for VulnerabilityRating {
    fn write_xml_element<W: std::io::Write>(
        &self,
        writer: &mut xml::EventWriter<W>,
    ) -> Result<(), crate::errors::XmlWriteError> {
        let vulnerability_rating_start_tag = XmlEvent::start_element(VULNERABILITY_RATING_TAG);

        writer
            .write(vulnerability_rating_start_tag)
            .map_err(to_xml_write_error(VULNERABILITY_RATING_TAG))?;

        if let Some(vulnerability_source) = &self.vulnerability_source {
            vulnerability_source.write_xml_element(writer)?;
        }

        if let Some(score) = &self.score {
            write_simple_tag(writer, SCORE_TAG, format!("{}", score).as_ref())?;
        }

        if let Some(severity) = &self.severity {
            write_simple_tag(writer, SEVERITY_TAG, &severity.0)?;
        }

        if let Some(score_method) = &self.score_method {
            write_simple_tag(writer, SCORE_METHOD_TAG, &score_method.0)?;
        }

        if let Some(vector) = &self.vector {
            write_simple_tag(writer, VECTOR_TAG, vector)?;
        }

        if let Some(justification) = &self.justification {
            write_simple_tag(writer, JUSTIFICATION_TAG, justification)?;
        }

        writer
            .write(XmlEvent::end_element())
            .map_err(to_xml_write_error(VULNERABILITY_RATING_TAG))?;

        Ok(())
    }
}

impl FromXml for VulnerabilityRating {
    fn read_xml_element<R: std::io::Read>(
        event_reader: &mut xml::EventReader<R>,
        element_name: &xml::name::OwnedName,
        _attributes: &[xml::attribute::OwnedAttribute],
    ) -> Result<Self, XmlReadError>
    where
        Self: Sized,
    {
        let mut vulnerability_source: Option<VulnerabilitySource> = None;
        let mut score: Option<f32> = None;
        let mut severity: Option<Severity> = None;
        let mut score_method: Option<ScoreMethod> = None;
        let mut vector: Option<String> = None;
        let mut justification: Option<String> = None;

        let mut got_end_tag = false;
        while !got_end_tag {
            let next_element = event_reader
                .next()
                .map_err(to_xml_read_error(VULNERABILITY_RATING_TAG))?;
            match next_element {
                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_SOURCE_TAG => {
                    vulnerability_source = Some(VulnerabilitySource::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

                reader::XmlEvent::StartElement { name, .. } if name.local_name == SCORE_TAG => {
                    score = Some(f32::from_xml_value(
                        SCORE_TAG,
                        read_simple_tag(event_reader, &name)?,
                    )?)
                }

                reader::XmlEvent::StartElement { name, .. } if name.local_name == SEVERITY_TAG => {
                    severity = Some(Severity(read_simple_tag(event_reader, &name)?))
                }

                reader::XmlEvent::StartElement { name, .. }
                    if name.local_name == SCORE_METHOD_TAG =>
                {
                    score_method = Some(ScoreMethod(read_simple_tag(event_reader, &name)?))
                }

                reader::XmlEvent::StartElement { name, .. } if name.local_name == VECTOR_TAG => {
                    vector = Some(read_simple_tag(event_reader, &name)?)
                }

                reader::XmlEvent::StartElement { name, .. }
                    if name.local_name == JUSTIFICATION_TAG =>
                {
                    justification = Some(read_simple_tag(event_reader, &name)?)
                }

                // lax validation of any elements from a different schema
                reader::XmlEvent::StartElement { name, .. } => {
                    read_lax_validation_tag(event_reader, &name)?
                }
                reader::XmlEvent::EndElement { name } if &name == element_name => {
                    got_end_tag = true;
                }
                unexpected => return Err(unexpected_element_error(element_name, unexpected)),
            }
        }

        Ok(Self {
            vulnerability_source,
            score,
            severity,
            score_method,
            vector,
            justification,
        })
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Severity(String);

impl From<models::vulnerability_rating::Severity> for Severity {
    fn from(other: models::vulnerability_rating::Severity) -> Self {
        Self(other.to_string())
    }
}

impl From<Severity> for models::vulnerability_rating::Severity {
    fn from(other: Severity) -> Self {
        models::vulnerability_rating::Severity::new_unchecked(other.0)
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct ScoreMethod(String);

impl From<models::vulnerability_rating::ScoreMethod> for ScoreMethod {
    fn from(other: models::vulnerability_rating::ScoreMethod) -> Self {
        Self(other.to_string())
    }
}

impl From<ScoreMethod> for models::vulnerability_rating::ScoreMethod {
    fn from(other: ScoreMethod) -> Self {
        models::vulnerability_rating::ScoreMethod::new_unchecked(other.0)
    }
}

#[cfg(test)]
pub(crate) mod test {
    use super::*;
    use pretty_assertions::assert_eq;

    use crate::{
        specs::common::vulnerability_source::test::{
            corresponding_vulnerability_source, example_vulnerability_source,
        },
        xml::test::{read_element_from_string, write_element_to_string},
    };

    pub(crate) fn example_vulnerability_ratings() -> VulnerabilityRatings {
        VulnerabilityRatings(vec![example_vulnerability_rating()])
    }

    pub(crate) fn corresponding_vulnerability_ratings(
    ) -> models::vulnerability_rating::VulnerabilityRatings {
        models::vulnerability_rating::VulnerabilityRatings(vec![
            corresponding_vulnerability_rating(),
        ])
    }

    pub(crate) fn example_vulnerability_rating() -> VulnerabilityRating {
        VulnerabilityRating {
            vulnerability_source: Some(example_vulnerability_source()),
            score: Some(9.8),
            severity: Some(Severity("info".to_string())),
            score_method: Some(ScoreMethod("CVSSv3".to_string())),
            vector: Some("vector".to_string()),
            justification: Some("justification".to_string()),
        }
    }

    pub(crate) fn corresponding_vulnerability_rating(
    ) -> models::vulnerability_rating::VulnerabilityRating {
        models::vulnerability_rating::VulnerabilityRating {
            vulnerability_source: Some(corresponding_vulnerability_source()),
            score: models::vulnerability_rating::Score::from_f32(9.8),
            severity: Some(models::vulnerability_rating::Severity::Info),
            score_method: Some(models::vulnerability_rating::ScoreMethod::CVSSv3),
            vector: Some(NormalizedString::new_unchecked("vector".to_string())),
            justification: Some("justification".to_string()),
        }
    }

    #[test]
    fn it_should_write_xml_full() {
        let xml_output = write_element_to_string(example_vulnerability_ratings());
        insta::assert_snapshot!(xml_output);
    }

    #[test]
    fn it_should_read_xml_full() {
        let input = r#"
<ratings>
  <rating>
    <source>
      <name>name</name>
      <url>url</url>
    </source>
    <score>9.8</score>
    <severity>info</severity>
    <method>CVSSv3</method>
    <vector>vector</vector>
    <justification>justification</justification>
  </rating>
</ratings>
"#;
        let actual: VulnerabilityRatings = read_element_from_string(input);
        let expected = example_vulnerability_ratings();
        assert_eq!(actual, expected);
    }
}
