/*
 * 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_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, ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::{reader, writer::XmlEvent};

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

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

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

const VULNERABILITY_REFERENCES_TAG: &str = "references";

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

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

        write_close_tag(writer, VULNERABILITY_REFERENCES_TAG)?;

        Ok(())
    }
}

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

// todo: check spec, in XML spec the fields are optional, in JSON spec the fields are required
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct VulnerabilityReference {
    id: String,
    #[serde(rename = "source")]
    vulnerability_source: VulnerabilitySource,
}

impl From<models::vulnerability_reference::VulnerabilityReference> for VulnerabilityReference {
    fn from(other: models::vulnerability_reference::VulnerabilityReference) -> Self {
        Self {
            id: other.id.to_string(),
            vulnerability_source: VulnerabilitySource::from(other.vulnerability_source),
        }
    }
}

impl From<VulnerabilityReference> for models::vulnerability_reference::VulnerabilityReference {
    fn from(other: VulnerabilityReference) -> Self {
        Self {
            id: NormalizedString::new_unchecked(other.id),
            vulnerability_source: models::vulnerability_source::VulnerabilitySource::from(
                other.vulnerability_source,
            ),
        }
    }
}

const VULNERABILITY_REFERENCE_TAG: &str = "reference";
const ID_TAG: &str = "id";
const VULNERABILITY_SOURCE_TAG: &str = "source";

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

        writer
            .write(vulnerability_reference_start_tag)
            .map_err(to_xml_write_error(VULNERABILITY_REFERENCE_TAG))?;

        write_simple_tag(writer, ID_TAG, &self.id)?;

        self.vulnerability_source.write_xml_element(writer)?;

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

        Ok(())
    }
}

impl FromXml for VulnerabilityReference {
    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 reference_id: Option<String> = None;
        let mut reference_source: Option<VulnerabilitySource> = None;

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

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VULNERABILITY_SOURCE_TAG => {
                    reference_source = Some(VulnerabilitySource::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)),
            }
        }

        let id = reference_id.ok_or_else(|| XmlReadError::RequiredDataMissing {
            required_field: ID_TAG.to_string(),
            element: element_name.local_name.to_string(),
        })?;

        let vulnerability_source =
            reference_source.ok_or_else(|| XmlReadError::RequiredDataMissing {
                required_field: VULNERABILITY_SOURCE_TAG.to_string(),
                element: element_name.local_name.to_string(),
            })?;

        Ok(Self {
            id,
            vulnerability_source,
        })
    }
}

#[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_references() -> VulnerabilityReferences {
        VulnerabilityReferences(vec![example_vulnerability_reference()])
    }

    pub(crate) fn corresponding_vulnerability_references(
    ) -> models::vulnerability_reference::VulnerabilityReferences {
        models::vulnerability_reference::VulnerabilityReferences(vec![
            corresponding_vulnerability_reference(),
        ])
    }

    pub(crate) fn example_vulnerability_reference() -> VulnerabilityReference {
        VulnerabilityReference {
            id: "id".to_string(),
            vulnerability_source: example_vulnerability_source(),
        }
    }

    pub(crate) fn corresponding_vulnerability_reference(
    ) -> models::vulnerability_reference::VulnerabilityReference {
        models::vulnerability_reference::VulnerabilityReference {
            id: NormalizedString::new_unchecked("id".to_string()),
            vulnerability_source: corresponding_vulnerability_source(),
        }
    }

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

    #[test]
    fn it_should_read_xml_full() {
        let input = r#"
<references>
  <reference>
    <id>id</id>
    <source>
      <name>name</name>
      <url>url</url>
    </source>
  </reference>
</references>
"#;
        let actual: VulnerabilityReferences = read_element_from_string(input);
        let expected = example_vulnerability_references();
        assert_eq!(actual, expected);
    }
}
