/*
 * 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 {
    #[versioned("1.5")]
    use crate::external_models::date_time::DateTime;
    use crate::utilities::convert_optional_vec;
    use crate::xml::{write_close_tag, write_start_tag};
    use crate::{
        errors::XmlReadError,
        models,
        utilities::convert_optional,
        xml::{
            read_lax_validation_tag, read_list_tag, read_simple_tag, to_xml_read_error,
            unexpected_element_error, write_simple_tag, FromXml, ToXml,
        },
    };
    use serde::{Deserialize, Serialize};
    use xml::reader;

    #[derive(Debug, Deserialize, Serialize, PartialEq)]
    #[serde(rename_all = "camelCase")]
    pub(crate) struct VulnerabilityAnalysis {
        #[serde(skip_serializing_if = "Option::is_none")]
        state: Option<ImpactAnalysisState>,
        #[serde(skip_serializing_if = "Option::is_none")]
        justification: Option<ImpactAnalysisJustification>,
        #[serde(skip_serializing_if = "Option::is_none")]
        responses: Option<Vec<ImpactAnalysisResponse>>,
        #[serde(skip_serializing_if = "Option::is_none")]
        detail: Option<String>,
        #[versioned("1.5")]
        #[serde(skip_serializing_if = "Option::is_none")]
        first_issued: Option<String>,
        #[versioned("1.5")]
        #[serde(skip_serializing_if = "Option::is_none")]
        last_updated: Option<String>,
    }

    impl From<models::vulnerability_analysis::VulnerabilityAnalysis> for VulnerabilityAnalysis {
        fn from(other: models::vulnerability_analysis::VulnerabilityAnalysis) -> Self {
            Self {
                state: convert_optional(other.state),
                justification: convert_optional(other.justification),
                responses: convert_optional_vec(other.responses),
                detail: other.detail,
                #[versioned("1.5")]
                first_issued: other.first_issued.map(|d| d.to_string()),
                #[versioned("1.5")]
                last_updated: other.last_updated.map(|d| d.to_string()),
            }
        }
    }

    impl From<VulnerabilityAnalysis> for models::vulnerability_analysis::VulnerabilityAnalysis {
        fn from(other: VulnerabilityAnalysis) -> Self {
            Self {
                state: convert_optional(other.state),
                justification: convert_optional(other.justification),
                responses: convert_optional_vec(other.responses),
                detail: other.detail,
                #[versioned("1.4")]
                first_issued: None,
                #[versioned("1.5")]
                first_issued: other.first_issued.map(DateTime),
                #[versioned("1.4")]
                last_updated: None,
                #[versioned("1.5")]
                last_updated: other.last_updated.map(DateTime),
            }
        }
    }

    const VULNERABILITY_ANALYSIS_TAG: &str = "analysis";
    const STATE_TAG: &str = "state";
    const JUSTIFICATION_TAG: &str = "justification";
    const RESPONSES_TAG: &str = "responses";
    const RESPONSE_TAG: &str = "response";
    const DETAIL_TAG: &str = "detail";
    #[versioned("1.5")]
    const FIRST_ISSUED_TAG: &str = "firstIssued";
    #[versioned("1.5")]
    const LAST_UPDATED_TAG: &str = "lastUpdated";

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

            if let Some(state) = &self.state {
                write_simple_tag(writer, STATE_TAG, &state.0)?;
            }

            if let Some(justification) = &self.justification {
                write_simple_tag(writer, JUSTIFICATION_TAG, &justification.0)?;
            }

            if let Some(responses) = &self.responses {
                write_start_tag(writer, RESPONSES_TAG)?;

                for response in responses {
                    write_simple_tag(writer, RESPONSE_TAG, &response.0)?;
                }

                write_close_tag(writer, RESPONSES_TAG)?;
            }

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

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

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

            write_close_tag(writer, VULNERABILITY_ANALYSIS_TAG)?;

            Ok(())
        }
    }

    impl FromXml for VulnerabilityAnalysis {
        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 state: Option<ImpactAnalysisState> = None;
            let mut justification: Option<ImpactAnalysisJustification> = None;
            let mut responses: Option<Vec<ImpactAnalysisResponse>> = None;
            let mut detail: Option<String> = None;
            #[versioned("1.5")]
            let mut first_issued: Option<String> = None;
            #[versioned("1.5")]
            let mut last_updated: Option<String> = None;

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

                    reader::XmlEvent::StartElement { name, .. }
                        if name.local_name == JUSTIFICATION_TAG =>
                    {
                        justification = Some(ImpactAnalysisJustification(read_simple_tag(
                            event_reader,
                            &name,
                        )?))
                    }

                    reader::XmlEvent::StartElement { name, .. }
                        if name.local_name == RESPONSES_TAG =>
                    {
                        responses = Some(
                            read_list_tag(event_reader, &name, RESPONSE_TAG)?
                                .into_iter()
                                .map(ImpactAnalysisResponse)
                                .collect(),
                        );
                    }

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

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

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

                    // 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 {
                state,
                justification,
                responses,
                detail,
                #[versioned("1.5")]
                first_issued,
                #[versioned("1.5")]
                last_updated,
            })
        }
    }

    #[derive(Debug, Deserialize, Serialize, PartialEq)]
    pub(crate) struct ImpactAnalysisState(String);

    impl From<models::vulnerability_analysis::ImpactAnalysisState> for ImpactAnalysisState {
        fn from(other: models::vulnerability_analysis::ImpactAnalysisState) -> Self {
            Self(other.to_string())
        }
    }

    impl From<ImpactAnalysisState> for models::vulnerability_analysis::ImpactAnalysisState {
        fn from(other: ImpactAnalysisState) -> Self {
            models::vulnerability_analysis::ImpactAnalysisState::new_unchecked(other.0)
        }
    }

    #[derive(Debug, Deserialize, Serialize, PartialEq)]
    pub(crate) struct ImpactAnalysisJustification(String);

    impl From<models::vulnerability_analysis::ImpactAnalysisJustification>
        for ImpactAnalysisJustification
    {
        fn from(other: models::vulnerability_analysis::ImpactAnalysisJustification) -> Self {
            Self(other.to_string())
        }
    }

    impl From<ImpactAnalysisJustification>
        for models::vulnerability_analysis::ImpactAnalysisJustification
    {
        fn from(other: ImpactAnalysisJustification) -> Self {
            models::vulnerability_analysis::ImpactAnalysisJustification::new_unchecked(other.0)
        }
    }

    #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
    pub struct ImpactAnalysisResponse(String);

    impl From<models::vulnerability_analysis::ImpactAnalysisResponse> for ImpactAnalysisResponse {
        fn from(other: models::vulnerability_analysis::ImpactAnalysisResponse) -> Self {
            Self(other.to_string())
        }
    }

    impl From<ImpactAnalysisResponse> for models::vulnerability_analysis::ImpactAnalysisResponse {
        fn from(other: ImpactAnalysisResponse) -> Self {
            models::vulnerability_analysis::ImpactAnalysisResponse::new_unchecked(other.0)
        }
    }

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

        #[versioned("1.5")]
        use crate::external_models::date_time::DateTime;
        use crate::xml::test::{read_element_from_string, write_element_to_string};

        #[versioned("1.4")]
        pub(crate) fn example_vulnerability_analysis() -> VulnerabilityAnalysis {
            VulnerabilityAnalysis {
                state: Some(ImpactAnalysisState("not_affected".to_string())),
                justification: Some(ImpactAnalysisJustification(
                    "code_not_reachable".to_string(),
                )),
                responses: Some(vec![ImpactAnalysisResponse("update".to_string())]),
                detail: Some("detail".to_string()),
            }
        }

        #[versioned("1.5")]
        pub(crate) fn example_vulnerability_analysis() -> VulnerabilityAnalysis {
            VulnerabilityAnalysis {
                state: Some(ImpactAnalysisState("not_affected".to_string())),
                justification: Some(ImpactAnalysisJustification(
                    "code_not_reachable".to_string(),
                )),
                responses: Some(vec![ImpactAnalysisResponse("update".to_string())]),
                detail: Some("detail".to_string()),
                first_issued: Some("2024-01-02".to_string()),
                last_updated: Some("2024-01-10".to_string()),
            }
        }

        #[versioned("1.4")]
        pub(crate) fn corresponding_vulnerability_analysis(
        ) -> models::vulnerability_analysis::VulnerabilityAnalysis {
            models::vulnerability_analysis::VulnerabilityAnalysis {
                state: Some(models::vulnerability_analysis::ImpactAnalysisState::NotAffected),
                justification: Some(
                    models::vulnerability_analysis::ImpactAnalysisJustification::CodeNotReachable,
                ),
                responses: Some(vec![
                    models::vulnerability_analysis::ImpactAnalysisResponse::Update,
                ]),
                detail: Some("detail".to_string()),
                first_issued: None,
                last_updated: None,
            }
        }

        #[versioned("1.5")]
        pub(crate) fn corresponding_vulnerability_analysis(
        ) -> models::vulnerability_analysis::VulnerabilityAnalysis {
            models::vulnerability_analysis::VulnerabilityAnalysis {
                state: Some(models::vulnerability_analysis::ImpactAnalysisState::NotAffected),
                justification: Some(
                    models::vulnerability_analysis::ImpactAnalysisJustification::CodeNotReachable,
                ),
                responses: Some(vec![
                    models::vulnerability_analysis::ImpactAnalysisResponse::Update,
                ]),
                detail: Some("detail".to_string()),
                first_issued: Some(DateTime("2024-01-02".to_string())),
                last_updated: Some(DateTime("2024-01-10".to_string())),
            }
        }

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

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