/*
 * 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::utilities::convert_optional_vec;
use crate::{
    errors::XmlReadError,
    models,
    utilities::convert_optional,
    xml::{
        read_lax_validation_tag, read_list_tag, read_simple_tag, to_xml_read_error,
        to_xml_write_error, unexpected_element_error, write_simple_tag, FromXml, ToXml,
    },
};
use serde::{Deserialize, Serialize};
use xml::{reader, writer::XmlEvent};

#[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>,
}

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,
        }
    }
}

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,
        }
    }
}

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";

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

        writer
            .write(vulnerability_analysis_start_tag)
            .map_err(to_xml_write_error(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 {
            writer
                .write(XmlEvent::start_element(RESPONSES_TAG))
                .map_err(to_xml_write_error(RESPONSES_TAG))?;
            for response in responses {
                write_simple_tag(writer, RESPONSE_TAG, &response.0)?;
            }
            writer
                .write(XmlEvent::end_element())
                .map_err(to_xml_write_error(RESPONSES_TAG))?;
        }

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

        writer
            .write(XmlEvent::end_element())
            .map_err(to_xml_write_error(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;

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

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

#[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 crate::xml::test::{read_element_from_string, write_element_to_string};

    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()),
        }
    }

    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()),
        }
    }

    #[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>
</analysis>
"#;
        let actual: VulnerabilityAnalysis = read_element_from_string(input);
        let expected = example_vulnerability_analysis();
        assert_eq!(actual, expected);
    }
}
