/*
 * 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::{date_time::DateTime, normalized_string::NormalizedString},
    models,
    utilities::{convert_optional, convert_optional_vec, convert_vec},
    xml::{
        optional_attribute, read_lax_validation_list_tag, read_lax_validation_tag, read_list_tag,
        read_optional_tag, read_simple_tag, to_xml_read_error, to_xml_write_error,
        unexpected_element_error, write_simple_tag, FromXml, ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::{reader, writer::XmlEvent};

use crate::specs::v1_4::{
    advisory::Advisories, property::Properties, tool::Tools,
    vulnerability_analysis::VulnerabilityAnalysis, vulnerability_credits::VulnerabilityCredits,
    vulnerability_rating::VulnerabilityRatings, vulnerability_reference::VulnerabilityReferences,
    vulnerability_source::VulnerabilitySource, vulnerability_target::VulnerabilityTargets,
};

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

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

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

const VULNERABILITIES_TAG: &str = "vulnerabilities";

impl ToXml for Vulnerabilities {
    fn write_xml_element<W: std::io::Write>(
        &self,
        writer: &mut xml::EventWriter<W>,
    ) -> Result<(), crate::errors::XmlWriteError> {
        writer
            .write(XmlEvent::start_element(VULNERABILITIES_TAG))
            .map_err(to_xml_write_error(VULNERABILITIES_TAG))?;

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

        writer
            .write(XmlEvent::end_element())
            .map_err(to_xml_write_error(VULNERABILITIES_TAG))?;
        Ok(())
    }
}

impl FromXml for Vulnerabilities {
    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_TAG)
            .map(Vulnerabilities)
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Vulnerability {
    #[serde(rename = "bom-ref", skip_serializing_if = "Option::is_none")]
    bom_ref: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    id: Option<String>,
    #[serde(rename = "source", skip_serializing_if = "Option::is_none")]
    vulnerability_source: Option<VulnerabilitySource>,
    #[serde(rename = "references", skip_serializing_if = "Option::is_none")]
    vulnerability_references: Option<VulnerabilityReferences>,
    #[serde(rename = "ratings", skip_serializing_if = "Option::is_none")]
    vulnerability_ratings: Option<VulnerabilityRatings>,
    #[serde(skip_serializing_if = "Option::is_none")]
    cwes: Option<Vec<u32>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    recommendation: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    advisories: Option<Advisories>,
    #[serde(skip_serializing_if = "Option::is_none")]
    created: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    published: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    updated: Option<String>,
    #[serde(rename = "credits", skip_serializing_if = "Option::is_none")]
    vulnerability_credits: Option<VulnerabilityCredits>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tools: Option<Tools>,
    #[serde(rename = "analysis", skip_serializing_if = "Option::is_none")]
    vulnerability_analysis: Option<VulnerabilityAnalysis>,
    #[serde(rename = "affects", skip_serializing_if = "Option::is_none")]
    vulnerability_targets: Option<VulnerabilityTargets>,
    #[serde(skip_serializing_if = "Option::is_none")]
    properties: Option<Properties>,
}

impl From<models::vulnerability::Vulnerability> for Vulnerability {
    fn from(other: models::vulnerability::Vulnerability) -> Self {
        Self {
            bom_ref: other.bom_ref,
            id: other.id.map(|i| i.to_string()),
            vulnerability_source: convert_optional(other.vulnerability_source),
            vulnerability_references: convert_optional(other.vulnerability_references),
            vulnerability_ratings: convert_optional(other.vulnerability_ratings),
            cwes: convert_optional_vec(other.cwes),
            description: other.description,
            detail: other.detail,
            recommendation: other.recommendation,
            advisories: convert_optional(other.advisories),
            created: other.created.map(|c| c.to_string()),
            published: other.published.map(|p| p.to_string()),
            updated: other.updated.map(|u| u.to_string()),
            vulnerability_credits: convert_optional(other.vulnerability_credits),
            tools: convert_optional(other.tools),
            vulnerability_analysis: convert_optional(other.vulnerability_analysis),
            vulnerability_targets: convert_optional(other.vulnerability_targets),
            properties: convert_optional(other.properties),
        }
    }
}

impl From<Vulnerability> for models::vulnerability::Vulnerability {
    fn from(other: Vulnerability) -> Self {
        Self {
            bom_ref: other.bom_ref,
            id: other.id.map(NormalizedString::new_unchecked),
            vulnerability_source: convert_optional(other.vulnerability_source),
            vulnerability_references: convert_optional(other.vulnerability_references),
            vulnerability_ratings: convert_optional(other.vulnerability_ratings),
            cwes: convert_optional_vec(other.cwes),
            description: other.description,
            detail: other.detail,
            recommendation: other.recommendation,
            advisories: convert_optional(other.advisories),
            created: other.created.map(DateTime),
            published: other.published.map(DateTime),
            updated: other.updated.map(DateTime),
            vulnerability_credits: convert_optional(other.vulnerability_credits),
            tools: convert_optional(other.tools),
            vulnerability_analysis: convert_optional(other.vulnerability_analysis),
            vulnerability_targets: convert_optional(other.vulnerability_targets),
            properties: convert_optional(other.properties),
        }
    }
}

const VULNERABILITY_TAG: &str = "vulnerability";
const BOM_REF_ATTR: &str = "bom-ref";
const ID_TAG: &str = "id";
const VULNERABILITY_SOURCE_TAG: &str = "source";
const VULNERABILITY_REFERENCES_TAG: &str = "references";
const VULNERABILITY_RATINGS_TAG: &str = "ratings";
const CWES_TAG: &str = "cwes";
const CWE_TAG: &str = "cwe";
const DESCRIPTION_TAG: &str = "description";
const DETAIL_TAG: &str = "detail";
const RECOMMENDATION_TAG: &str = "recommendation";
const ADVISORIES_TAG: &str = "advisories";
const CREATED_TAG: &str = "created";
const PUBLISHED_TAG: &str = "published";
const UPDATED_TAG: &str = "updated";
const VULNERABILITY_CREDITS_TAG: &str = "credits";
const TOOLS_TAG: &str = "tools";
const VULNERABILITY_ANALYSIS_TAG: &str = "analysis";
const VULNERABILITY_TARGETS_TAG: &str = "affects";
const PROPERTIES_TAG: &str = "properties";

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

        if let Some(bom_ref) = &self.bom_ref {
            vulnerability_start_tag = vulnerability_start_tag.attr(BOM_REF_ATTR, bom_ref);
        }

        writer
            .write(vulnerability_start_tag)
            .map_err(to_xml_write_error(VULNERABILITY_TAG))?;

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

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

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

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

        if let Some(cwes) = &self.cwes {
            writer
                .write(XmlEvent::start_element(CWES_TAG))
                .map_err(to_xml_write_error(CWES_TAG))?;
            for &cwe in cwes {
                write_simple_tag(writer, CWE_TAG, format!("{}", cwe).as_ref())?;
            }
            writer
                .write(XmlEvent::end_element())
                .map_err(to_xml_write_error(CWES_TAG))?;
        }

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

        if let Some(detail) = &self.detail {
            write_simple_tag(writer, DETAIL_TAG, detail)?;
        } else {
            write_simple_tag(writer, DETAIL_TAG, "")?;
        }

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

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

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

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

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

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

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

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

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

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

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

        Ok(())
    }
}

impl FromXml for Vulnerability {
    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 bom_ref = optional_attribute(attributes, BOM_REF_ATTR);

        let mut id: Option<String> = None;
        let mut vulnerability_source: Option<VulnerabilitySource> = None;
        let mut vulnerability_references: Option<VulnerabilityReferences> = None;
        let mut vulnerability_ratings: Option<VulnerabilityRatings> = None;
        let mut cwes: Option<Vec<u32>> = None;
        let mut description: Option<String> = None;
        let mut detail: Option<String> = None;
        let mut recommendation: Option<String> = None;
        let mut advisories: Option<Advisories> = None;
        let mut created: Option<String> = None;
        let mut published: Option<String> = None;
        let mut updated: Option<String> = None;
        let mut vulnerability_credits: Option<VulnerabilityCredits> = None;
        let mut tools: Option<Tools> = None;
        let mut vulnerability_analysis: Option<VulnerabilityAnalysis> = None;
        let mut vulnerability_targets: Option<VulnerabilityTargets> = None;
        let mut properties: Option<Properties> = None;

        let mut got_end_tag = false;
        while !got_end_tag {
            let next_element = event_reader
                .next()
                .map_err(to_xml_read_error(VULNERABILITY_TAG))?;
            match next_element {
                reader::XmlEvent::StartElement { name, .. } if name.local_name == ID_TAG => {
                    id = Some(read_simple_tag(event_reader, &name)?);
                }

                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, attributes, ..
                } if name.local_name == VULNERABILITY_REFERENCES_TAG => {
                    vulnerability_references = Some(VulnerabilityReferences::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?);
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_RATINGS_TAG => {
                    vulnerability_ratings = Some(VulnerabilityRatings::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?);
                }

                reader::XmlEvent::StartElement { name, .. } if name.local_name == CWES_TAG => {
                    cwes = Some(read_list_tag(event_reader, &name, CWE_TAG)?);
                }

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

                reader::XmlEvent::StartElement { name, .. } if name.local_name == DETAIL_TAG => {
                    detail = read_optional_tag(event_reader, &name)?;
                }

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

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == ADVISORIES_TAG => {
                    advisories = Some(Advisories::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

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

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

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

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_CREDITS_TAG => {
                    vulnerability_credits = Some(VulnerabilityCredits::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == TOOLS_TAG => {
                    tools = Some(Tools::read_xml_element(event_reader, &name, &attributes)?)
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_ANALYSIS_TAG => {
                    vulnerability_analysis = Some(VulnerabilityAnalysis::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_TARGETS_TAG => {
                    vulnerability_targets = Some(VulnerabilityTargets::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == PROPERTIES_TAG => {
                    properties = Some(Properties::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?)
                }

                // 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 {
            bom_ref,
            id,
            vulnerability_source,
            vulnerability_references,
            vulnerability_ratings,
            cwes,
            description,
            detail,
            recommendation,
            advisories,
            created,
            published,
            updated,
            vulnerability_credits,
            tools,
            vulnerability_analysis,
            vulnerability_targets,
            properties,
        })
    }
}

#[cfg(test)]
pub(crate) mod test {
    use super::*;
    use crate::{
        specs::v1_4::{
            advisory::test::{corresponding_advisories, example_advisories},
            property::test::{corresponding_properties, example_properties},
            tool::test::{corresponding_tools, example_tools},
            vulnerability_analysis::test::{
                corresponding_vulnerability_analysis, example_vulnerability_analysis,
            },
            vulnerability_credits::test::{
                corresponding_vulnerability_credits, example_vulnerability_credits,
            },
            vulnerability_rating::test::{
                corresponding_vulnerability_ratings, example_vulnerability_ratings,
            },
            vulnerability_reference::test::{
                corresponding_vulnerability_references, example_vulnerability_references,
            },
            vulnerability_source::test::{
                corresponding_vulnerability_source, example_vulnerability_source,
            },
            vulnerability_target::test::{
                corresponding_vulnerability_targets, example_vulnerability_targets,
            },
        },
        xml::test::{read_element_from_string, write_element_to_string},
    };

    pub(crate) fn example_vulnerabilities() -> Vulnerabilities {
        Vulnerabilities(vec![example_vulnerability()])
    }

    pub(crate) fn corresponding_vulnerabilities() -> models::vulnerability::Vulnerabilities {
        models::vulnerability::Vulnerabilities(vec![corresponding_vulnerability()])
    }

    pub(crate) fn example_vulnerability() -> Vulnerability {
        Vulnerability {
            bom_ref: Some("bom-ref".to_string()),
            id: Some("id".to_string()),
            vulnerability_source: Some(example_vulnerability_source()),
            vulnerability_references: Some(example_vulnerability_references()),
            vulnerability_ratings: Some(example_vulnerability_ratings()),
            cwes: Some(vec![1, 2, 3]),
            description: Some("description".to_string()),
            detail: Some("detail".to_string()),
            recommendation: Some("recommendation".to_string()),
            advisories: Some(example_advisories()),
            created: Some("created".to_string()),
            published: Some("published".to_string()),
            updated: Some("updated".to_string()),
            vulnerability_credits: Some(example_vulnerability_credits()),
            tools: Some(example_tools()),
            vulnerability_analysis: Some(example_vulnerability_analysis()),
            vulnerability_targets: Some(example_vulnerability_targets()),
            properties: Some(example_properties()),
        }
    }

    pub(crate) fn corresponding_vulnerability() -> models::vulnerability::Vulnerability {
        models::vulnerability::Vulnerability {
            bom_ref: Some("bom-ref".to_string()),
            id: Some(NormalizedString::new_unchecked("id".to_string())),
            vulnerability_source: Some(corresponding_vulnerability_source()),
            vulnerability_references: Some(corresponding_vulnerability_references()),
            vulnerability_ratings: Some(corresponding_vulnerability_ratings()),
            cwes: Some(vec![1, 2, 3]),
            description: Some("description".to_string()),
            detail: Some("detail".to_string()),
            recommendation: Some("recommendation".to_string()),
            advisories: Some(corresponding_advisories()),
            created: Some(DateTime("created".to_string())),
            published: Some(DateTime("published".to_string())),
            updated: Some(DateTime("updated".to_string())),
            vulnerability_credits: Some(corresponding_vulnerability_credits()),
            tools: Some(corresponding_tools()),
            vulnerability_analysis: Some(corresponding_vulnerability_analysis()),
            vulnerability_targets: Some(corresponding_vulnerability_targets()),
            properties: Some(corresponding_properties()),
        }
    }

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

    #[test]
    fn it_should_read_xml_full() {
        let input = r#"
<vulnerabilities>
  <vulnerability bom-ref="bom-ref">
    <id>id</id>
    <source>
      <name>name</name>
      <url>url</url>
    </source>
    <references>
      <reference>
        <id>id</id>
        <source>
          <name>name</name>
          <url>url</url>
        </source>
      </reference>
    </references>
    <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>
    <cwes>
      <cwe>1</cwe>
      <cwe>2</cwe>
      <cwe>3</cwe>
    </cwes>
    <description>description</description>
    <detail>detail</detail>
    <recommendation>recommendation</recommendation>
    <advisories>
      <advisory>
        <title>title</title>
        <url>url</url>
      </advisory>
    </advisories>
    <created>created</created>
    <published>published</published>
    <updated>updated</updated>
    <credits>
      <organizations>
        <organization>
          <name>name</name>
          <url>url</url>
          <contact>
            <name>name</name>
            <email>email</email>
            <phone>phone</phone>
          </contact>
        </organization>
      </organizations>
      <individuals>
        <individual>
          <name>name</name>
          <email>email</email>
          <phone>phone</phone>
        </individual>
      </individuals>
    </credits>
    <tools>
      <tool>
        <vendor>vendor</vendor>
        <name>name</name>
        <version>version</version>
        <hashes>
          <hash alg="algorithm">hash value</hash>
        </hashes>
      </tool>
    </tools>
    <analysis>
      <state>not_affected</state>
      <justification>code_not_reachable</justification>
      <responses>
        <response>update</response>
      </responses>
      <detail>detail</detail>
    </analysis>
    <affects>
      <target>
        <ref>ref</ref>
        <versions>
          <version>
            <version>5.0.0</version>
            <status>unaffected</status>
          </version>
          <version>
            <range>vers:npm/1.2.3|>=2.0.0|&lt;5.0.0</range>
            <status>affected</status>
          </version>
        </versions>
      </target>
    </affects>
    <properties>
      <property name="name">value</property>
    </properties>
  </vulnerability>
</vulnerabilities>
"#;
        let actual: Vulnerabilities = read_element_from_string(input);
        let expected = example_vulnerabilities();
        assert_eq!(actual, expected);
    }
}
