/*
 * 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 cyclonedx_bom_macros::versioned;

#[versioned("1.4", "1.5")]
pub(crate) mod base {

    use crate::{
        errors::{BomError, XmlReadError},
        external_models::{date_time::DateTime, normalized_string::NormalizedString},
        models::{self},
        utilities::{
            convert_optional, convert_optional_vec, convert_vec, try_convert_optional,
            try_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_close_tag, write_simple_tag,
            write_start_tag, FromXml, ToXml,
        },
    };
    use serde::{Deserialize, Serialize};
    use xml::{reader, writer::XmlEvent};

    use crate::specs::common::{
        advisory::Advisories, property::Properties, vulnerability_credits::VulnerabilityCredits,
        vulnerability_rating::VulnerabilityRatings,
        vulnerability_reference::VulnerabilityReferences,
        vulnerability_source::VulnerabilitySource, vulnerability_target::VulnerabilityTargets,
    };
    #[versioned("1.4")]
    use crate::specs::v1_4::{tool::Tools, vulnerability_analysis::VulnerabilityAnalysis};
    #[versioned("1.5")]
    use crate::specs::v1_5::{
        proof_of_concept::ProofOfConcept, tool::Tools,
        vulnerability_analysis::VulnerabilityAnalysis,
    };

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

    impl TryFrom<models::vulnerability::Vulnerabilities> for Vulnerabilities {
        type Error = BomError;

        fn try_from(other: models::vulnerability::Vulnerabilities) -> Result<Self, Self::Error> {
            try_convert_vec(other.0).map(Self)
        }
    }

    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> {
            write_start_tag(writer, VULNERABILITIES_TAG)?;

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

            write_close_tag(writer, 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>,
        #[versioned("1.5")]
        #[serde(skip_serializing_if = "Option::is_none")]
        workaround: Option<String>,
        #[versioned("1.5")]
        #[serde(skip_serializing_if = "Option::is_none")]
        proof_of_concept: Option<ProofOfConcept>,
        #[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>,
        #[versioned("1.5")]
        #[serde(skip_serializing_if = "Option::is_none")]
        rejected: 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 TryFrom<models::vulnerability::Vulnerability> for Vulnerability {
        type Error = BomError;

        fn try_from(other: models::vulnerability::Vulnerability) -> Result<Self, Self::Error> {
            Ok(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,
                #[versioned("1.5")]
                workaround: convert_optional(other.workaround),
                #[versioned("1.5")]
                proof_of_concept: convert_optional(other.proof_of_concept),
                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()),
                #[versioned("1.5")]
                rejected: other.rejected.map(|r| r.to_string()),
                vulnerability_credits: convert_optional(other.vulnerability_credits),
                tools: try_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,
                #[versioned("1.4")]
                workaround: None,
                #[versioned("1.5")]
                workaround: convert_optional(other.workaround),
                #[versioned("1.4")]
                proof_of_concept: None,
                #[versioned("1.5")]
                proof_of_concept: convert_optional(other.proof_of_concept),
                advisories: convert_optional(other.advisories),
                created: other.created.map(DateTime),
                published: other.published.map(DateTime),
                updated: other.updated.map(DateTime),
                #[versioned("1.4")]
                rejected: None,
                #[versioned("1.5")]
                rejected: other.rejected.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";
    #[versioned("1.5")]
    const REJECTED_TAG: &str = "rejected";
    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";
    #[versioned("1.5")]
    const WORKAROUND_TAG: &str = "workaround";
    #[versioned("1.5")]
    const PROOF_OF_CONCEPT_TAG: &str = "proofOfConcept";

    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 {
                write_start_tag(writer, CWES_TAG)?;

                for &cwe in cwes {
                    write_simple_tag(writer, CWE_TAG, format!("{}", cwe).as_ref())?;
                }

                write_close_tag(writer, 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)?;
            }

            #[versioned("1.5")]
            if let Some(rejected) = &self.rejected {
                write_simple_tag(writer, REJECTED_TAG, rejected)?;
            }

            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)?;
            }

            #[versioned("1.5")]
            if let Some(workaround) = &self.workaround {
                write_simple_tag(writer, WORKAROUND_TAG, workaround)?;
            }

            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;
            #[versioned("1.5")]
            let mut rejected: 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;
            #[versioned("1.5")]
            let mut workaround: Option<String> = None;
            #[versioned("1.5")]
            let mut proof_of_concept: Option<ProofOfConcept> = 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)?);
                    }

                    #[versioned("1.5")]
                    reader::XmlEvent::StartElement { name, .. }
                        if name.local_name == REJECTED_TAG =>
                    {
                        rejected = 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,
                        )?)
                    }

                    #[versioned("1.5")]
                    reader::XmlEvent::StartElement { name, .. }
                        if name.local_name == WORKAROUND_TAG =>
                    {
                        workaround = Some(read_simple_tag(event_reader, &name)?);
                    }

                    #[versioned("1.5")]
                    reader::XmlEvent::StartElement {
                        name, attributes, ..
                    } if name.local_name == PROOF_OF_CONCEPT_TAG => {
                        proof_of_concept = Some(ProofOfConcept::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,
                #[versioned("1.5")]
                workaround,
                #[versioned("1.5")]
                proof_of_concept,
                advisories,
                created,
                published,
                updated,
                #[versioned("1.5")]
                rejected,
                vulnerability_credits,
                tools,
                vulnerability_analysis,
                vulnerability_targets,
                properties,
            })
        }
    }

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

        #[versioned("1.4")]
        use crate::specs::v1_4::{
            tool::test::{corresponding_tools, example_tools},
            vulnerability_analysis::test::{
                corresponding_vulnerability_analysis, example_vulnerability_analysis,
            },
        };
        #[versioned("1.5")]
        use crate::specs::v1_5::{
            proof_of_concept::test::{corresponding_proof_of_concept, example_proof_of_concept},
            tool::test::{corresponding_tools, example_tools},
            vulnerability_analysis::test::{
                corresponding_vulnerability_analysis, example_vulnerability_analysis,
            },
        };
        use crate::{
            specs::common::{
                advisory::test::{corresponding_advisories, example_advisories},
                property::test::{corresponding_properties, example_properties},
                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()])
        }

        #[versioned("1.4")]
        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()),
            }
        }

        #[versioned("1.5")]
        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()),
                workaround: Some("workaround".to_string()),
                proof_of_concept: Some(example_proof_of_concept()),
                advisories: Some(example_advisories()),
                created: Some("created".to_string()),
                published: Some("published".to_string()),
                updated: Some("updated".to_string()),
                rejected: Some("rejected".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()),
            }
        }

        #[versioned("1.4")]
        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()),
                workaround: None,
                proof_of_concept: None,
                advisories: Some(corresponding_advisories()),
                created: Some(DateTime("created".to_string())),
                published: Some(DateTime("published".to_string())),
                updated: Some(DateTime("updated".to_string())),
                rejected: None,
                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()),
            }
        }

        #[versioned("1.5")]
        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()),
                workaround: Some("workaround".to_string()),
                proof_of_concept: Some(corresponding_proof_of_concept()),
                advisories: Some(corresponding_advisories()),
                created: Some(DateTime("created".to_string())),
                published: Some(DateTime("published".to_string())),
                updated: Some(DateTime("updated".to_string())),
                rejected: Some(DateTime("rejected".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);
        }

        #[versioned("1.4")]
        #[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>
        <externalReferences>
          <reference type="external reference type">
            <url>url</url>
            <comment>comment</comment>
            <hashes>
              <hash alg="algorithm">hash value</hash>
            </hashes>
          </reference>
        </externalReferences>
      </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);
        }

        #[versioned("1.5")]
        #[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>
    <workaround>workaround</workaround>
    <proofOfConcept>
      <reproductionSteps>reproduction steps</reproductionSteps>
      <environment>production</environment>
      <supportingMaterial>
        <attachment content-type="image/jpeg" encoding="base64">abcdefgh</attachment>
      </supportingMaterial>
    </proofOfConcept>
    <advisories>
      <advisory>
        <title>title</title>
        <url>url</url>
      </advisory>
    </advisories>
    <created>created</created>
    <published>published</published>
    <updated>updated</updated>
    <rejected>rejected</rejected>
    <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>
        <externalReferences>
          <reference type="external reference type">
            <url>url</url>
            <comment>comment</comment>
            <hashes>
              <hash alg="algorithm">hash value</hash>
            </hashes>
          </reference>
        </externalReferences>
      </tool>
    </tools>
    <analysis>
      <state>not_affected</state>
      <justification>code_not_reachable</justification>
      <responses>
        <response>update</response>
      </responses>
      <detail>detail</detail>
      <firstIssued>2024-01-02</firstIssued>
      <lastUpdated>2024-01-10</lastUpdated>
    </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);
        }
    }
}
