/*
 * 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,
    utilities::{convert_optional, convert_vec},
    xml::{
        read_lax_validation_list_tag, read_lax_validation_tag, read_simple_tag, to_xml_read_error,
        unexpected_element_error, write_close_tag, write_simple_tag, write_start_tag, FromXml,
        ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::reader;

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

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

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

const VULNERABILITY_TARGETS_TAG: &str = "affects";

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

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

        write_close_tag(writer, VULNERABILITY_TARGETS_TAG)?;

        Ok(())
    }
}

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

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct VulnerabilityTarget {
    #[serde(rename = "ref")]
    bom_ref: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    versions: Option<Versions>,
}

impl From<models::vulnerability_target::VulnerabilityTarget> for VulnerabilityTarget {
    fn from(other: models::vulnerability_target::VulnerabilityTarget) -> Self {
        Self {
            bom_ref: other.bom_ref,
            versions: convert_optional(other.versions),
        }
    }
}

impl From<VulnerabilityTarget> for models::vulnerability_target::VulnerabilityTarget {
    fn from(other: VulnerabilityTarget) -> Self {
        Self {
            bom_ref: other.bom_ref,
            versions: convert_optional(other.versions),
        }
    }
}

const VULNERABILITY_TARGET_TAG: &str = "target";
const REF_TAG: &str = "ref";
const VERSIONS_TAG: &str = "versions";

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

        // TODO: should not be a tag, but an attribute
        write_simple_tag(writer, REF_TAG, &self.bom_ref)?;

        if let Some(versions) = &self.versions {
            write_start_tag(writer, VERSIONS_TAG)?;

            for version in versions.0.iter() {
                version.write_xml_element(writer)?;
            }

            write_close_tag(writer, VERSIONS_TAG)?;
        }

        write_close_tag(writer, VULNERABILITY_TARGET_TAG)?;

        Ok(())
    }
}

impl FromXml for VulnerabilityTarget {
    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 bom_ref: Option<String> = None;
        let mut versions = None;

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

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VERSIONS_TAG => {
                    versions = Some(Versions::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 bom_ref = bom_ref.ok_or_else(|| XmlReadError::RequiredDataMissing {
            required_field: REF_TAG.to_string(),
            element: element_name.local_name.to_string(),
        })?;

        Ok(Self { bom_ref, versions })
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(transparent)]
struct Versions(Vec<Version>);

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

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

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

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

        write_close_tag(writer, VERSIONS_TAG)?;

        Ok(())
    }
}

const VERSION_TAG: &str = "version";

impl FromXml for Versions {
    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, VERSION_TAG).map(Versions)
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Version {
    #[serde(flatten)]
    version_range: VersionRange,
    status: Status,
}

impl From<models::vulnerability_target::Version> for Version {
    fn from(other: models::vulnerability_target::Version) -> Self {
        Self {
            version_range: other.version_range.into(),
            status: other.status.into(),
        }
    }
}

impl From<Version> for models::vulnerability_target::Version {
    fn from(other: Version) -> Self {
        Self {
            version_range: other.version_range.into(),
            status: other.status.into(),
        }
    }
}

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

        self.version_range.write_xml_element(writer)?;
        self.status.write_xml_element(writer)?;

        write_close_tag(writer, VERSION_TAG)?;

        Ok(())
    }
}

const RANGE_TAG: &str = "range";

impl FromXml for Version {
    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 version_range: Option<VersionRange> = None;
        let mut status: Option<Status> = None;

        let mut got_end_tag = false;
        while !got_end_tag {
            let next_element = event_reader
                .next()
                .map_err(to_xml_read_error(VERSION_TAG))?;
            match next_element {
                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == VERSION_TAG || name.local_name == RANGE_TAG => {
                    version_range = Some(VersionRange::read_xml_element(
                        event_reader,
                        &name,
                        &attributes,
                    )?);
                }

                reader::XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == STATUS_TAG => {
                    status = Some(Status::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 version_range = version_range.ok_or_else(|| XmlReadError::RequiredDataMissing {
            required_field: format!("{} || {}", VERSION_TAG, RANGE_TAG),
            element: element_name.local_name.to_string(),
        })?;

        let status = status.unwrap_or(Status("affected".to_string()));

        Ok(Self {
            version_range,
            status,
        })
    }
}

#[allow(clippy::enum_variant_names)]
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
enum VersionRange {
    Version(String),
    Range(String),
    UndefinedVersionRange(String),
}

impl From<models::vulnerability_target::VersionRange> for VersionRange {
    fn from(other: models::vulnerability_target::VersionRange) -> Self {
        match other {
            models::vulnerability_target::VersionRange::Version(v) => Self::Version(v.to_string()),
            models::vulnerability_target::VersionRange::Range(r) => Self::Range(r.to_string()),
            models::vulnerability_target::VersionRange::UndefinedVersionRange(u) => {
                Self::UndefinedVersionRange(u)
            }
        }
    }
}

impl From<VersionRange> for models::vulnerability_target::VersionRange {
    fn from(other: VersionRange) -> Self {
        match other {
            VersionRange::Version(v) => Self::Version(NormalizedString::new_unchecked(v)),
            VersionRange::Range(r) => Self::Range(NormalizedString::new_unchecked(r)),
            VersionRange::UndefinedVersionRange(u) => Self::UndefinedVersionRange(u),
        }
    }
}

impl ToXml for VersionRange {
    fn write_xml_element<W: std::io::Write>(
        &self,
        writer: &mut xml::EventWriter<W>,
    ) -> Result<(), crate::errors::XmlWriteError> {
        match self {
            VersionRange::Version(v) => {
                write_simple_tag(writer, VERSION_TAG, v)?;
            }
            VersionRange::Range(r) => {
                write_simple_tag(writer, RANGE_TAG, r)?;
            }
            _ => {}
        }

        Ok(())
    }
}

impl FromXml for VersionRange {
    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,
    {
        match element_name.local_name.as_ref() {
            VERSION_TAG => Ok(Self::Version(read_simple_tag(event_reader, element_name)?)),
            RANGE_TAG => Ok(Self::Range(read_simple_tag(event_reader, element_name)?)),
            unexpected => Err(XmlReadError::UnexpectedElementReadError {
                error: format!("Got unexpected element {:?}", unexpected),
                element: "VersionRange".to_string(),
            }),
        }
    }
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct Status(String);

impl From<models::vulnerability_target::Status> for Status {
    fn from(other: models::vulnerability_target::Status) -> Self {
        match other {
            models::vulnerability_target::Status::Affected => Self("affected".to_string()),
            models::vulnerability_target::Status::Unaffected => Self("unaffected".to_string()),
            models::vulnerability_target::Status::Unknown => Self("unknown".to_string()),
            models::vulnerability_target::Status::UndefinedStatus(undefined) => Self(undefined),
        }
    }
}

impl From<Status> for models::vulnerability_target::Status {
    fn from(other: Status) -> Self {
        match other.0.as_str() {
            "affected" => Self::Affected,
            "unaffected" => Self::Unaffected,
            "unknown" => Self::Unknown,
            undefined => Self::UndefinedStatus(undefined.to_string()),
        }
    }
}

const STATUS_TAG: &str = "status";

impl ToXml for Status {
    fn write_xml_element<W: std::io::Write>(
        &self,
        writer: &mut xml::EventWriter<W>,
    ) -> Result<(), crate::errors::XmlWriteError> {
        write_simple_tag(writer, STATUS_TAG, &self.0)
    }
}

impl FromXml for Status {
    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_simple_tag(event_reader, element_name).map(Status)
    }
}

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

    use crate::{
        external_models::normalized_string::NormalizedString,
        xml::test::{read_element_from_string, write_element_to_string},
    };

    pub(crate) fn example_vulnerability_targets() -> VulnerabilityTargets {
        VulnerabilityTargets(vec![example_vulnerability_target()])
    }

    pub(crate) fn example_vulnerability_target() -> VulnerabilityTarget {
        VulnerabilityTarget {
            bom_ref: "ref".to_string(),
            versions: Some(Versions(vec![
                Version {
                    version_range: VersionRange::Version("5.0.0".to_string()),
                    status: Status("unaffected".to_string()),
                },
                Version {
                    version_range: VersionRange::Range("vers:npm/1.2.3|>=2.0.0|<5.0.0".to_string()),
                    status: Status("affected".to_string()),
                },
            ])),
        }
    }

    pub(crate) fn corresponding_vulnerability_targets(
    ) -> models::vulnerability_target::VulnerabilityTargets {
        models::vulnerability_target::VulnerabilityTargets(vec![
            corresponding_vulnerability_target(),
        ])
    }

    pub(crate) fn corresponding_vulnerability_target(
    ) -> models::vulnerability_target::VulnerabilityTarget {
        models::vulnerability_target::VulnerabilityTarget {
            bom_ref: "ref".to_string(),
            versions: Some(models::vulnerability_target::Versions(vec![
                models::vulnerability_target::Version {
                    version_range: models::vulnerability_target::VersionRange::Version(
                        NormalizedString::new_unchecked("5.0.0".to_string()),
                    ),
                    status: models::vulnerability_target::Status::Unaffected,
                },
                models::vulnerability_target::Version {
                    version_range: models::vulnerability_target::VersionRange::Range(
                        NormalizedString::new_unchecked(
                            "vers:npm/1.2.3|>=2.0.0|<5.0.0".to_string(),
                        ),
                    ),
                    status: models::vulnerability_target::Status::Affected,
                },
            ])),
        }
    }

    fn example_versions_defined() -> Versions {
        Versions(vec![
            example_version_with_affected_version(),
            example_version_with_affected_range(),
            example_version_with_unaffected_version(),
            example_version_with_unaffected_range(),
            example_version_with_unknown_version(),
            example_version_with_unknown_range(),
        ])
    }

    fn example_version_with_affected_version() -> Version {
        Version {
            version_range: example_version_range_with_version(),
            status: example_status_affected(),
        }
    }

    fn example_version_with_unaffected_version() -> Version {
        Version {
            version_range: example_version_range_with_version(),
            status: example_status_unaffected(),
        }
    }

    fn example_version_with_unknown_version() -> Version {
        Version {
            version_range: example_version_range_with_version(),
            status: example_status_unknown(),
        }
    }

    fn example_version_with_affected_range() -> Version {
        Version {
            version_range: example_version_range_with_range(),
            status: example_status_affected(),
        }
    }

    fn example_version_with_unaffected_range() -> Version {
        Version {
            version_range: example_version_range_with_range(),
            status: example_status_unaffected(),
        }
    }

    fn example_version_with_unknown_range() -> Version {
        Version {
            version_range: example_version_range_with_range(),
            status: example_status_unknown(),
        }
    }

    fn example_version_range_with_version() -> VersionRange {
        VersionRange::Version("5.0.0".to_string())
    }

    fn example_version_range_with_range() -> VersionRange {
        VersionRange::Range("vers:npm/1.2.3|>=2.0.0|<5.0.0".to_string())
    }

    fn example_status_affected() -> Status {
        Status("affected".to_string())
    }

    fn example_status_unaffected() -> Status {
        Status("unaffected".to_string())
    }

    fn example_status_unknown() -> Status {
        Status("unknown".to_string())
    }

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

    #[test]
    fn read_xml_versions_defined() {
        let input = r#"
<versions>
  <version>
    <version>5.0.0</version>
    <status>affected</status>
  </version>
  <version>
    <range>vers:npm/1.2.3|>=2.0.0|&lt;5.0.0</range>
    <status>affected</status>
  </version>
  <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>unaffected</status>
  </version>
  <version>
    <version>5.0.0</version>
    <status>unknown</status>
  </version>
  <version>
    <range>vers:npm/1.2.3|>=2.0.0|&lt;5.0.0</range>
    <status>unknown</status>
  </version>
</versions>
"#;
        let actual: Versions = read_element_from_string(input);
        let expected = example_versions_defined();
        assert_eq!(actual, expected);
    }

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

    // todo: check encoding for '>', see '<' => '&lt;'
    #[test]
    fn it_should_read_xml_full() {
        let input = r#"
<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>
        <!-- default value for status is "affected" -->
      </version>
    </versions>
  </target>
</affects>
"#;
        let actual: VulnerabilityTargets = read_element_from_string(input);
        let expected = example_vulnerability_targets();
        assert_eq!(actual, expected);
    }
}
