/*
 * 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,
    models,
    utilities::convert_optional_vec,
    xml::{
        read_lax_validation_tag, read_list_tag, to_xml_read_error, unexpected_element_error,
        write_close_tag, write_start_tag, FromXml, ToInnerXml, ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::reader;

use crate::specs::common::organization::{OrganizationalContact, OrganizationalEntity};

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct VulnerabilityCredits {
    #[serde(skip_serializing_if = "Option::is_none")]
    organizations: Option<Vec<OrganizationalEntity>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    individuals: Option<Vec<OrganizationalContact>>,
}

impl From<models::vulnerability_credits::VulnerabilityCredits> for VulnerabilityCredits {
    fn from(other: models::vulnerability_credits::VulnerabilityCredits) -> Self {
        Self {
            organizations: convert_optional_vec(other.organizations),
            individuals: convert_optional_vec(other.individuals),
        }
    }
}

impl From<VulnerabilityCredits> for models::vulnerability_credits::VulnerabilityCredits {
    fn from(other: VulnerabilityCredits) -> Self {
        Self {
            organizations: convert_optional_vec(other.organizations),
            individuals: convert_optional_vec(other.individuals),
        }
    }
}

const VULNERABILITY_CREDITS_TAG: &str = "credits";
const ORGANIZATIONS_TAG: &str = "organizations";
const ORGANIZATION_TAG: &str = "organization";
const INDIVIDUALS_TAG: &str = "individuals";
const INDIVIDUAL_TAG: &str = "individual";

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

        if let Some(organizations) = &self.organizations {
            write_start_tag(writer, ORGANIZATIONS_TAG)?;

            for organization in organizations {
                if organization.will_write() {
                    organization.write_xml_named_element(writer, ORGANIZATION_TAG)?;
                }
            }

            write_close_tag(writer, ORGANIZATIONS_TAG)?;
        }

        if let Some(individuals) = &self.individuals {
            write_start_tag(writer, INDIVIDUALS_TAG)?;

            for individual in individuals {
                if individual.will_write() {
                    individual.write_xml_named_element(writer, INDIVIDUAL_TAG)?;
                }
            }

            write_close_tag(writer, INDIVIDUALS_TAG)?;
        }

        write_close_tag(writer, VULNERABILITY_CREDITS_TAG)?;

        Ok(())
    }
}

impl FromXml for VulnerabilityCredits {
    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 organizations: Option<Vec<OrganizationalEntity>> = None;
        let mut individuals: Option<Vec<OrganizationalContact>> = None;

        let mut got_end_tag = false;
        while !got_end_tag {
            let next_element = event_reader
                .next()
                .map_err(to_xml_read_error(VULNERABILITY_CREDITS_TAG))?;
            match next_element {
                reader::XmlEvent::StartElement { name, .. }
                    if name.local_name == ORGANIZATIONS_TAG =>
                {
                    organizations = Some(read_list_tag(event_reader, &name, ORGANIZATION_TAG)?);
                }

                reader::XmlEvent::StartElement { name, .. }
                    if name.local_name == INDIVIDUALS_TAG =>
                {
                    individuals = Some(read_list_tag(event_reader, &name, INDIVIDUAL_TAG)?);
                }

                // 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 {
            organizations,
            individuals,
        })
    }
}

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

    use crate::{
        specs::common::organization::test::{
            corresponding_contact, corresponding_entity, example_contact, example_entity,
        },
        xml::test::{read_element_from_string, write_element_to_string},
    };

    pub(crate) fn example_vulnerability_credits() -> VulnerabilityCredits {
        VulnerabilityCredits {
            organizations: Some(vec![example_entity()]),
            individuals: Some(vec![example_contact()]),
        }
    }

    pub(crate) fn corresponding_vulnerability_credits(
    ) -> models::vulnerability_credits::VulnerabilityCredits {
        models::vulnerability_credits::VulnerabilityCredits {
            organizations: Some(vec![corresponding_entity()]),
            individuals: Some(vec![corresponding_contact()]),
        }
    }

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

    #[test]
    fn it_should_read_xml_full() {
        let input = r#"
<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>
"#;
        let actual: VulnerabilityCredits = read_element_from_string(input);
        let expected = example_vulnerability_credits();
        assert_eq!(actual, expected);
    }
}
