// Copyright (c) The nextest Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::{
    DeserializeError, DeserializeErrorKind, NonSuccessKind, PathElement, Property, Report,
    ReportUuid, TestCase, TestCaseStatus, TestRerun, TestSuite, XmlString,
};
use chrono::{DateTime, FixedOffset};
use indexmap::IndexMap;
use newtype_uuid::GenericUuid;
use quick_xml::{
    escape::{resolve_xml_entity, unescape_with},
    events::{BytesStart, Event},
    Reader,
};
use std::{io::BufRead, time::Duration};

impl Report {
    /// **Experimental**: Deserializes a JUnit XML report from a reader.
    ///
    /// The deserializer should work with JUnit reports generated by the
    /// `quick-junit` crate, but might not work with JUnit reports generated by
    /// other tools. Patches to fix this are welcome.
    ///
    /// # Errors
    ///
    /// Returns an error if the XML is malformed, or if required attributes are
    /// missing.
    pub fn deserialize<R: BufRead>(reader: R) -> Result<Self, DeserializeError> {
        let mut xml_reader = Reader::from_reader(reader);
        xml_reader.config_mut().trim_text(false);
        deserialize_report(&mut xml_reader)
    }

    /// Deserializes a JUnit XML report from a string.
    ///
    /// # Errors
    ///
    /// Returns an error if the XML is malformed, or if required attributes are
    /// missing.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use quick_junit::Report;
    ///
    /// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
    /// <testsuites name="my-test-run" tests="1" failures="0" errors="0">
    ///     <testsuite name="my-test-suite" tests="1" disabled="0" errors="0" failures="0">
    ///         <testcase name="success-case"/>
    ///     </testsuite>
    /// </testsuites>
    /// "#;
    ///
    /// let report = Report::deserialize_from_str(xml).unwrap();
    /// assert_eq!(report.name.as_str(), "my-test-run");
    /// assert_eq!(report.tests, 1);
    /// ```
    pub fn deserialize_from_str(xml: &str) -> Result<Self, DeserializeError> {
        Self::deserialize(xml.as_bytes())
    }
}

/// Deserializes a Report from XML.
fn deserialize_report<R: BufRead>(reader: &mut Reader<R>) -> Result<Report, DeserializeError> {
    let mut buf = Vec::new();
    let mut report: Option<Report> = None;
    let mut properly_closed = false;
    let root_path = vec![PathElement::TestSuites];

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) if e.name().as_ref() == b"testsuites" => {
                let mut name = None;
                let mut uuid = None;
                let mut timestamp = None;
                let mut time = None;
                let mut tests = 0;
                let mut failures = 0;
                let mut errors = 0;

                for attr in e.attributes() {
                    let attr = attr.map_err(|e| {
                        DeserializeError::new(DeserializeErrorKind::AttrError(e), root_path.clone())
                    })?;
                    let mut attr_path = root_path.clone();
                    match attr.key.as_ref() {
                        b"name" => {
                            attr_path.push(PathElement::Attribute("name".to_string()));
                            name = Some(parse_xml_string(&attr.value, &attr_path)?);
                        }
                        b"uuid" => {
                            attr_path.push(PathElement::Attribute("uuid".to_string()));
                            uuid = Some(parse_uuid(&attr.value, &attr_path)?);
                        }
                        b"timestamp" => {
                            attr_path.push(PathElement::Attribute("timestamp".to_string()));
                            timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
                        }
                        b"time" => {
                            attr_path.push(PathElement::Attribute("time".to_string()));
                            time = Some(parse_duration(&attr.value, &attr_path)?);
                        }
                        b"tests" => {
                            attr_path.push(PathElement::Attribute("tests".to_string()));
                            tests = parse_usize(&attr.value, &attr_path)?;
                        }
                        b"failures" => {
                            attr_path.push(PathElement::Attribute("failures".to_string()));
                            failures = parse_usize(&attr.value, &attr_path)?;
                        }
                        b"errors" => {
                            attr_path.push(PathElement::Attribute("errors".to_string()));
                            errors = parse_usize(&attr.value, &attr_path)?;
                        }
                        _ => {} // Ignore unknown attributes
                    }
                }

                let name = name.ok_or_else(|| {
                    let mut attr_path = root_path.clone();
                    attr_path.push(PathElement::Attribute("name".to_string()));
                    DeserializeError::new(
                        DeserializeErrorKind::MissingAttribute("name".to_string()),
                        attr_path,
                    )
                })?;

                let test_suites = Vec::new();

                report = Some(Report {
                    name,
                    uuid,
                    timestamp,
                    time,
                    tests,
                    failures,
                    errors,
                    test_suites,
                });
            }
            Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuites" => {
                let mut name = None;
                let mut uuid = None;
                let mut timestamp = None;
                let mut time = None;
                let mut tests = 0;
                let mut failures = 0;
                let mut errors = 0;

                for attr in e.attributes() {
                    let attr = attr.map_err(|e| {
                        DeserializeError::new(DeserializeErrorKind::AttrError(e), root_path.clone())
                    })?;
                    let mut attr_path = root_path.clone();
                    match attr.key.as_ref() {
                        b"name" => {
                            attr_path.push(PathElement::Attribute("name".to_string()));
                            name = Some(parse_xml_string(&attr.value, &attr_path)?);
                        }
                        b"uuid" => {
                            attr_path.push(PathElement::Attribute("uuid".to_string()));
                            uuid = Some(parse_uuid(&attr.value, &attr_path)?);
                        }
                        b"timestamp" => {
                            attr_path.push(PathElement::Attribute("timestamp".to_string()));
                            timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
                        }
                        b"time" => {
                            attr_path.push(PathElement::Attribute("time".to_string()));
                            time = Some(parse_duration(&attr.value, &attr_path)?);
                        }
                        b"tests" => {
                            attr_path.push(PathElement::Attribute("tests".to_string()));
                            tests = parse_usize(&attr.value, &attr_path)?;
                        }
                        b"failures" => {
                            attr_path.push(PathElement::Attribute("failures".to_string()));
                            failures = parse_usize(&attr.value, &attr_path)?;
                        }
                        b"errors" => {
                            attr_path.push(PathElement::Attribute("errors".to_string()));
                            errors = parse_usize(&attr.value, &attr_path)?;
                        }
                        _ => {} // Ignore unknown attributes
                    }
                }

                let name = name.ok_or_else(|| {
                    let mut attr_path = root_path.clone();
                    attr_path.push(PathElement::Attribute("name".to_string()));
                    DeserializeError::new(
                        DeserializeErrorKind::MissingAttribute("name".to_string()),
                        attr_path,
                    )
                })?;

                report = Some(Report {
                    name,
                    uuid,
                    timestamp,
                    time,
                    tests,
                    failures,
                    errors,
                    test_suites: Vec::new(),
                });
                properly_closed = true; // Empty elements are self-closing
            }
            Ok(Event::Start(e)) if e.name().as_ref() == b"testsuite" => {
                if let Some(ref mut report) = report {
                    let suite_index = report.test_suites.len();
                    let test_suite = deserialize_test_suite(reader, &e, &root_path, suite_index)?;
                    report.test_suites.push(test_suite);
                }
            }
            Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuite" => {
                if let Some(ref mut report) = report {
                    let suite_index = report.test_suites.len();
                    let test_suite = deserialize_test_suite_empty(&e, &root_path, suite_index)?;
                    report.test_suites.push(test_suite);
                }
            }
            Ok(Event::End(e)) if e.name().as_ref() == b"testsuites" => {
                properly_closed = true;
                break;
            }
            Ok(Event::Eof) => break,
            Ok(_) => {}
            Err(e) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::XmlError(e),
                    root_path.clone(),
                ))
            }
        }
        buf.clear();
    }

    if !properly_closed && report.is_some() {
        return Err(DeserializeError::new(
            DeserializeErrorKind::InvalidStructure(
                "unexpected EOF, <testsuites> not properly closed".to_string(),
            ),
            root_path,
        ));
    }

    report.ok_or_else(|| {
        DeserializeError::new(
            DeserializeErrorKind::InvalidStructure("missing <testsuites> element".to_string()),
            Vec::new(),
        )
    })
}

/// Deserializes a TestSuite from XML (for <testsuite> start tag).
fn deserialize_test_suite<R: BufRead>(
    reader: &mut Reader<R>,
    start_element: &BytesStart<'_>,
    path: &[PathElement],
    suite_index: usize,
) -> Result<TestSuite, DeserializeError> {
    let mut name = None;
    let mut tests = 0;
    let mut disabled = 0;
    let mut errors = 0;
    let mut failures = 0;
    let mut timestamp = None;
    let mut time = None;
    let mut extra = IndexMap::new();

    // First pass: extract name and other attributes
    for attr in start_element.attributes() {
        let attr = attr.map_err(|e| {
            let mut suite_path = path.to_vec();
            suite_path.push(PathElement::TestSuite(suite_index, None));
            DeserializeError::new(DeserializeErrorKind::AttrError(e), suite_path)
        })?;
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestSuite(suite_index, None));
        match attr.key.as_ref() {
            b"name" => {
                attr_path.push(PathElement::Attribute("name".to_string()));
                name = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"tests" => {
                attr_path.push(PathElement::Attribute("tests".to_string()));
                tests = parse_usize(&attr.value, &attr_path)?;
            }
            b"disabled" => {
                attr_path.push(PathElement::Attribute("disabled".to_string()));
                disabled = parse_usize(&attr.value, &attr_path)?;
            }
            b"errors" => {
                attr_path.push(PathElement::Attribute("errors".to_string()));
                errors = parse_usize(&attr.value, &attr_path)?;
            }
            b"failures" => {
                attr_path.push(PathElement::Attribute("failures".to_string()));
                failures = parse_usize(&attr.value, &attr_path)?;
            }
            b"timestamp" => {
                attr_path.push(PathElement::Attribute("timestamp".to_string()));
                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
            }
            b"time" => {
                attr_path.push(PathElement::Attribute("time".to_string()));
                time = Some(parse_duration(&attr.value, &attr_path)?);
            }
            _ => {
                // Store unknown attributes in extra
                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
                let value = parse_xml_string(&attr.value, &attr_path)?;
                extra.insert(key, value);
            }
        }
    }

    let name_value = name.clone().ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestSuite(suite_index, None));
        attr_path.push(PathElement::Attribute("name".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("name".to_string()),
            attr_path,
        )
    })?;

    // Build the test suite path with the name
    let mut suite_path = path.to_vec();
    suite_path.push(PathElement::TestSuite(
        suite_index,
        Some(name_value.as_str().to_string()),
    ));

    let mut test_cases = Vec::new();
    let mut properties = Vec::new();
    let mut system_out = None;
    let mut system_err = None;
    let mut buf = Vec::new();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) => {
                let element_name = e.name().as_ref().to_vec();
                if &element_name == b"testcase" {
                    let test_case =
                        deserialize_test_case(reader, e, &suite_path, test_cases.len())?;
                    test_cases.push(test_case);
                } else if &element_name == b"properties" {
                    properties = deserialize_properties(reader, &suite_path)?;
                } else if &element_name == b"system-out" {
                    let mut child_path = suite_path.clone();
                    child_path.push(PathElement::SystemOut);
                    system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
                } else if &element_name == b"system-err" {
                    let mut child_path = suite_path.clone();
                    child_path.push(PathElement::SystemErr);
                    system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
                } else {
                    // Skip unknown elements.
                    let tag_name = e.name().to_owned();
                    reader
                        .read_to_end_into(tag_name, &mut Vec::new())
                        .map_err(|e| {
                            DeserializeError::new(
                                DeserializeErrorKind::XmlError(e),
                                suite_path.clone(),
                            )
                        })?;
                }
            }
            Ok(Event::Empty(ref e)) => {
                if e.name().as_ref() == b"testcase" {
                    let test_case = deserialize_test_case_empty(e, &suite_path, test_cases.len())?;
                    test_cases.push(test_case);
                }
            }
            Ok(Event::End(ref e)) if e.name().as_ref() == b"testsuite" => break,
            Ok(Event::Eof) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::InvalidStructure(
                        "unexpected EOF in <testsuite>".to_string(),
                    ),
                    suite_path,
                ))
            }
            Ok(_) => {}
            Err(e) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::XmlError(e),
                    suite_path,
                ))
            }
        }
        buf.clear();
    }

    Ok(TestSuite {
        name: name_value,
        tests,
        disabled,
        errors,
        failures,
        timestamp,
        time,
        test_cases,
        properties,
        system_out,
        system_err,
        extra,
    })
}

/// Deserializes an empty TestSuite from XML (for <testsuite/> empty tag).
fn deserialize_test_suite_empty(
    element: &BytesStart<'_>,
    path: &[PathElement],
    suite_index: usize,
) -> Result<TestSuite, DeserializeError> {
    let mut name = None;
    let mut tests = 0;
    let mut disabled = 0;
    let mut errors = 0;
    let mut failures = 0;
    let mut timestamp = None;
    let mut time = None;
    let mut extra = IndexMap::new();

    // First pass: extract name and other attributes
    for attr in element.attributes() {
        let attr = attr.map_err(|e| {
            let mut suite_path = path.to_vec();
            suite_path.push(PathElement::TestSuite(suite_index, None));
            DeserializeError::new(DeserializeErrorKind::AttrError(e), suite_path)
        })?;
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestSuite(suite_index, None));
        match attr.key.as_ref() {
            b"name" => {
                attr_path.push(PathElement::Attribute("name".to_string()));
                name = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"tests" => {
                attr_path.push(PathElement::Attribute("tests".to_string()));
                tests = parse_usize(&attr.value, &attr_path)?;
            }
            b"disabled" => {
                attr_path.push(PathElement::Attribute("disabled".to_string()));
                disabled = parse_usize(&attr.value, &attr_path)?;
            }
            b"errors" => {
                attr_path.push(PathElement::Attribute("errors".to_string()));
                errors = parse_usize(&attr.value, &attr_path)?;
            }
            b"failures" => {
                attr_path.push(PathElement::Attribute("failures".to_string()));
                failures = parse_usize(&attr.value, &attr_path)?;
            }
            b"timestamp" => {
                attr_path.push(PathElement::Attribute("timestamp".to_string()));
                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
            }
            b"time" => {
                attr_path.push(PathElement::Attribute("time".to_string()));
                time = Some(parse_duration(&attr.value, &attr_path)?);
            }
            _ => {
                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
                let value = parse_xml_string(&attr.value, &attr_path)?;
                extra.insert(key, value);
            }
        }
    }

    let name = name.ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestSuite(suite_index, None));
        attr_path.push(PathElement::Attribute("name".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("name".to_string()),
            attr_path,
        )
    })?;

    Ok(TestSuite {
        name,
        tests,
        disabled,
        errors,
        failures,
        timestamp,
        time,
        test_cases: Vec::new(),
        properties: Vec::new(),
        system_out: None,
        system_err: None,
        extra,
    })
}

/// Deserializes a TestCase from XML (for <testcase> start tag).
fn deserialize_test_case<R: BufRead>(
    reader: &mut Reader<R>,
    start_element: &BytesStart<'_>,
    path: &[PathElement],
    case_index: usize,
) -> Result<TestCase, DeserializeError> {
    let mut name = None;
    let mut classname = None;
    let mut assertions = None;
    let mut timestamp = None;
    let mut time = None;
    let mut extra = IndexMap::new();

    // First pass: extract name and other attributes
    for attr in start_element.attributes() {
        let attr = attr.map_err(|e| {
            let mut case_path = path.to_vec();
            case_path.push(PathElement::TestCase(case_index, None));
            DeserializeError::new(DeserializeErrorKind::AttrError(e), case_path)
        })?;
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestCase(case_index, None));
        match attr.key.as_ref() {
            b"name" => {
                attr_path.push(PathElement::Attribute("name".to_string()));
                name = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"classname" => {
                attr_path.push(PathElement::Attribute("classname".to_string()));
                classname = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"assertions" => {
                attr_path.push(PathElement::Attribute("assertions".to_string()));
                assertions = Some(parse_usize(&attr.value, &attr_path)?);
            }
            b"timestamp" => {
                attr_path.push(PathElement::Attribute("timestamp".to_string()));
                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
            }
            b"time" => {
                attr_path.push(PathElement::Attribute("time".to_string()));
                time = Some(parse_duration(&attr.value, &attr_path)?);
            }
            _ => {
                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
                let value = parse_xml_string(&attr.value, &attr_path)?;
                extra.insert(key, value);
            }
        }
    }

    let name_value = name.clone().ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestCase(case_index, None));
        attr_path.push(PathElement::Attribute("name".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("name".to_string()),
            attr_path,
        )
    })?;

    // Build the test case path with the name
    let mut case_path = path.to_vec();
    case_path.push(PathElement::TestCase(
        case_index,
        Some(name_value.as_str().to_string()),
    ));

    let mut properties = Vec::new();
    let mut system_out = None;
    let mut system_err = None;
    let mut status_elements = Vec::new();
    let mut buf = Vec::new();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) => {
                let element_name = e.name().as_ref().to_vec();
                let is_status_element = matches!(
                    element_name.as_slice(),
                    b"failure"
                        | b"error"
                        | b"skipped"
                        | b"flakyFailure"
                        | b"flakyError"
                        | b"rerunFailure"
                        | b"rerunError"
                );

                if is_status_element {
                    let status_element = deserialize_status_element(reader, e, false, &case_path)?;
                    status_elements.push(status_element);
                } else if &element_name == b"properties" {
                    properties = deserialize_properties(reader, &case_path)?;
                } else if &element_name == b"system-out" {
                    let mut child_path = case_path.clone();
                    child_path.push(PathElement::SystemOut);
                    system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
                } else if &element_name == b"system-err" {
                    let mut child_path = case_path.clone();
                    child_path.push(PathElement::SystemErr);
                    system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
                } else {
                    // Skip unknown Start elements
                    let tag_name = e.name().to_owned();
                    reader
                        .read_to_end_into(tag_name, &mut Vec::new())
                        .map_err(|e| {
                            DeserializeError::new(
                                DeserializeErrorKind::XmlError(e),
                                case_path.clone(),
                            )
                        })?;
                }
            }
            Ok(Event::Empty(ref e)) => {
                let element_name = e.name().as_ref().to_vec();
                let is_status_element = matches!(
                    element_name.as_slice(),
                    b"failure"
                        | b"error"
                        | b"skipped"
                        | b"flakyFailure"
                        | b"flakyError"
                        | b"rerunFailure"
                        | b"rerunError"
                );

                if is_status_element {
                    let status_element = deserialize_status_element(reader, e, true, &case_path)?;
                    status_elements.push(status_element);
                }
                // Empty elements don't need special handling for properties, system-out, system-err
            }
            Ok(Event::End(ref e)) if e.name().as_ref() == b"testcase" => break,
            Ok(Event::Eof) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::InvalidStructure(
                        "unexpected EOF in <testcase>".to_string(),
                    ),
                    case_path,
                ))
            }
            Ok(_) => {}
            Err(e) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::XmlError(e),
                    case_path,
                ))
            }
        }
        buf.clear();
    }

    let status = build_test_case_status(status_elements, &case_path)?;

    Ok(TestCase {
        name: name_value,
        classname,
        assertions,
        timestamp,
        time,
        status,
        system_out,
        system_err,
        extra,
        properties,
    })
}

/// Deserializes an empty TestCase from XML (for <testcase/> empty tag).
fn deserialize_test_case_empty(
    element: &BytesStart<'_>,
    path: &[PathElement],
    case_index: usize,
) -> Result<TestCase, DeserializeError> {
    let mut name = None;
    let mut classname = None;
    let mut assertions = None;
    let mut timestamp = None;
    let mut time = None;
    let mut extra = IndexMap::new();

    for attr in element.attributes() {
        let attr = attr.map_err(|e| {
            let mut case_path = path.to_vec();
            case_path.push(PathElement::TestCase(case_index, None));
            DeserializeError::new(DeserializeErrorKind::AttrError(e), case_path)
        })?;
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestCase(case_index, None));
        match attr.key.as_ref() {
            b"name" => {
                attr_path.push(PathElement::Attribute("name".to_string()));
                name = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"classname" => {
                attr_path.push(PathElement::Attribute("classname".to_string()));
                classname = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"assertions" => {
                attr_path.push(PathElement::Attribute("assertions".to_string()));
                assertions = Some(parse_usize(&attr.value, &attr_path)?);
            }
            b"timestamp" => {
                attr_path.push(PathElement::Attribute("timestamp".to_string()));
                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
            }
            b"time" => {
                attr_path.push(PathElement::Attribute("time".to_string()));
                time = Some(parse_duration(&attr.value, &attr_path)?);
            }
            _ => {
                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
                let value = parse_xml_string(&attr.value, &attr_path)?;
                extra.insert(key, value);
            }
        }
    }

    let name_value = name.ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::TestCase(case_index, None));
        attr_path.push(PathElement::Attribute("name".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("name".to_string()),
            attr_path,
        )
    })?;

    Ok(TestCase {
        name: name_value,
        classname,
        assertions,
        timestamp,
        time,
        status: TestCaseStatus::success(),
        system_out: None,
        system_err: None,
        extra,
        properties: Vec::new(),
    })
}

/// Represents a parsed status element (failure, error, skipped, etc.)
#[derive(Debug)]
/// Common data for all status elements
struct StatusElementData {
    message: Option<XmlString>,
    ty: Option<XmlString>,
    description: Option<XmlString>,
    stack_trace: Option<XmlString>,
    system_out: Option<XmlString>,
    system_err: Option<XmlString>,
    timestamp: Option<DateTime<FixedOffset>>,
    time: Option<Duration>,
}

/// Main status element kind (failure, error, or skipped)
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum MainStatusKind {
    Failure,
    Error,
    Skipped,
}

/// Main status element
struct MainStatusElement {
    kind: MainStatusKind,
    data: StatusElementData,
}

/// Rerun/flaky status element kind (failure or error)
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum RerunStatusKind {
    Failure,
    Error,
}

/// Rerun or flaky status element
struct RerunStatusElement {
    kind: RerunStatusKind,
    data: StatusElementData,
}

/// Categorized status element
enum StatusElement {
    Main(MainStatusElement),
    Flaky(RerunStatusElement),
    Rerun(RerunStatusElement),
}

enum StatusCategory {
    Main(MainStatusKind),
    Flaky(RerunStatusKind),
    Rerun(RerunStatusKind),
}

/// Deserializes a status element (failure, error, skipped, flaky*, rerun*).
fn deserialize_status_element<R: BufRead>(
    reader: &mut Reader<R>,
    element: &BytesStart<'_>,
    is_empty: bool,
    path: &[PathElement],
) -> Result<StatusElement, DeserializeError> {
    let (category, status_path_elem) = match element.name().as_ref() {
        b"failure" => (
            StatusCategory::Main(MainStatusKind::Failure),
            PathElement::Failure,
        ),
        b"error" => (
            StatusCategory::Main(MainStatusKind::Error),
            PathElement::Error,
        ),
        b"skipped" => (
            StatusCategory::Main(MainStatusKind::Skipped),
            PathElement::Skipped,
        ),
        b"flakyFailure" => (
            StatusCategory::Flaky(RerunStatusKind::Failure),
            PathElement::FlakyFailure,
        ),
        b"flakyError" => (
            StatusCategory::Flaky(RerunStatusKind::Error),
            PathElement::FlakyError,
        ),
        b"rerunFailure" => (
            StatusCategory::Rerun(RerunStatusKind::Failure),
            PathElement::RerunFailure,
        ),
        b"rerunError" => (
            StatusCategory::Rerun(RerunStatusKind::Error),
            PathElement::RerunError,
        ),
        _ => {
            return Err(DeserializeError::new(
                DeserializeErrorKind::UnexpectedElement(
                    String::from_utf8_lossy(element.name().as_ref()).to_string(),
                ),
                path.to_vec(),
            ))
        }
    };

    let mut status_path = path.to_vec();
    status_path.push(status_path_elem);

    let mut message = None;
    let mut ty = None;
    let mut timestamp = None;
    let mut time = None;

    for attr in element.attributes() {
        let attr = attr.map_err(|e| {
            DeserializeError::new(DeserializeErrorKind::AttrError(e), status_path.clone())
        })?;
        let mut attr_path = status_path.clone();
        match attr.key.as_ref() {
            b"message" => {
                attr_path.push(PathElement::Attribute("message".to_string()));
                message = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"type" => {
                attr_path.push(PathElement::Attribute("type".to_string()));
                ty = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"timestamp" => {
                attr_path.push(PathElement::Attribute("timestamp".to_string()));
                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
            }
            b"time" => {
                attr_path.push(PathElement::Attribute("time".to_string()));
                time = Some(parse_duration(&attr.value, &attr_path)?);
            }
            _ => {} // Ignore unknown attributes
        }
    }

    let mut description_text = String::new();
    let mut stack_trace = None;
    let mut system_out = None;
    let mut system_err = None;

    // Only read child content if this is not an empty element.
    if !is_empty {
        let mut buf = Vec::new();
        loop {
            match reader.read_event_into(&mut buf) {
                Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
                    let element_name = e.name().as_ref().to_vec();
                    if &element_name == b"stackTrace" {
                        let mut child_path = status_path.clone();
                        child_path.push(PathElement::Attribute("stackTrace".to_string()));
                        stack_trace = Some(read_text_content(reader, b"stackTrace", &child_path)?);
                    } else if &element_name == b"system-out" {
                        let mut child_path = status_path.clone();
                        child_path.push(PathElement::SystemOut);
                        system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
                    } else if &element_name == b"system-err" {
                        let mut child_path = status_path.clone();
                        child_path.push(PathElement::SystemErr);
                        system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
                    } else {
                        // Skip unknown Start elements
                        let tag_name = e.name().to_owned();
                        reader
                            .read_to_end_into(tag_name, &mut Vec::new())
                            .map_err(|e| {
                                DeserializeError::new(
                                    DeserializeErrorKind::XmlError(e),
                                    status_path.clone(),
                                )
                            })?;
                    }
                }
                Ok(Event::Text(ref e)) => {
                    let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
                        DeserializeError::new(
                            DeserializeErrorKind::Utf8Error(e),
                            status_path.clone(),
                        )
                    })?;
                    // Unescape XML entities in the text content and accumulate
                    let unescaped = unescape_with(text, resolve_xml_entity).map_err(|e| {
                        DeserializeError::new(
                            DeserializeErrorKind::EscapeError(e),
                            status_path.clone(),
                        )
                    })?;
                    description_text.push_str(&unescaped);
                }
                Ok(Event::CData(ref e)) => {
                    // CDATA sections are already unescaped, just accumulate
                    let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
                        DeserializeError::new(
                            DeserializeErrorKind::Utf8Error(e),
                            status_path.clone(),
                        )
                    })?;
                    description_text.push_str(text);
                }
                Ok(Event::GeneralRef(ref e)) => {
                    // Handle entity references like &quot;, &amp;, etc.
                    let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
                        DeserializeError::new(
                            DeserializeErrorKind::Utf8Error(e),
                            status_path.clone(),
                        )
                    })?;
                    let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
                        DeserializeError::new(
                            DeserializeErrorKind::InvalidStructure(format!(
                                "unrecognized entity: {entity_name}",
                            )),
                            status_path.clone(),
                        )
                    })?;
                    description_text.push_str(unescaped);
                }
                Ok(Event::End(ref e))
                    if matches!(
                        e.name().as_ref(),
                        b"failure"
                            | b"error"
                            | b"skipped"
                            | b"flakyFailure"
                            | b"flakyError"
                            | b"rerunFailure"
                            | b"rerunError"
                    ) =>
                {
                    break
                }
                Ok(Event::Eof) => {
                    return Err(DeserializeError::new(
                        DeserializeErrorKind::InvalidStructure(
                            "unexpected EOF in status element".to_string(),
                        ),
                        status_path,
                    ))
                }
                Ok(_) => {}
                Err(e) => {
                    return Err(DeserializeError::new(
                        DeserializeErrorKind::XmlError(e),
                        status_path,
                    ))
                }
            }
            buf.clear();
        }
    }

    // Convert accumulated text to final description, trimming whitespace
    let description = if !description_text.trim().is_empty() {
        Some(XmlString::new(description_text.trim()))
    } else {
        None
    };

    let data = StatusElementData {
        message,
        ty,
        description,
        stack_trace,
        system_out,
        system_err,
        timestamp,
        time,
    };

    Ok(match category {
        StatusCategory::Main(kind) => StatusElement::Main(MainStatusElement { kind, data }),
        StatusCategory::Flaky(kind) => StatusElement::Flaky(RerunStatusElement { kind, data }),
        StatusCategory::Rerun(kind) => StatusElement::Rerun(RerunStatusElement { kind, data }),
    })
}

/// Builds a TestCaseStatus from parsed status elements.
fn build_test_case_status(
    status_elements: Vec<StatusElement>,
    path: &[PathElement],
) -> Result<TestCaseStatus, DeserializeError> {
    if status_elements.is_empty() {
        return Ok(TestCaseStatus::success());
    }

    // Separate the main status from reruns and flaky runs.
    let mut main_status: Option<&MainStatusElement> = None;
    let mut flaky_runs = Vec::new();
    let mut reruns = Vec::new();

    for element in &status_elements {
        match element {
            StatusElement::Main(main) => {
                if main_status.is_some() {
                    return Err(DeserializeError::new(
                        DeserializeErrorKind::InvalidStructure(
                            "multiple main status elements (failure/error/skipped) are not allowed"
                                .to_string(),
                        ),
                        path.to_vec(),
                    ));
                }
                main_status = Some(main);
            }
            StatusElement::Flaky(flaky) => {
                flaky_runs.push(flaky);
            }
            StatusElement::Rerun(rerun) => {
                reruns.push(rerun);
            }
        }
    }

    // Build the status.
    if let Some(main) = main_status {
        match main.kind {
            MainStatusKind::Skipped => Ok(TestCaseStatus::Skipped {
                message: main.data.message.clone(),
                ty: main.data.ty.clone(),
                description: main.data.description.clone(),
            }),
            MainStatusKind::Failure | MainStatusKind::Error => {
                let kind = if main.kind == MainStatusKind::Failure {
                    NonSuccessKind::Failure
                } else {
                    NonSuccessKind::Error
                };

                let reruns = reruns.into_iter().map(build_test_rerun).collect();

                Ok(TestCaseStatus::NonSuccess {
                    kind,
                    message: main.data.message.clone(),
                    ty: main.data.ty.clone(),
                    description: main.data.description.clone(),
                    reruns,
                })
            }
        }
    } else if !flaky_runs.is_empty() {
        // Success with flaky runs
        let flaky_runs = flaky_runs.into_iter().map(build_test_rerun).collect();

        Ok(TestCaseStatus::Success { flaky_runs })
    } else {
        Err(DeserializeError::new(
            DeserializeErrorKind::InvalidStructure("invalid status elements".to_string()),
            path.to_vec(),
        ))
    }
}

/// Builds a TestRerun from a rerun status element.
///
/// The type system ensures only flaky/rerun elements can be passed here.
fn build_test_rerun(element: &RerunStatusElement) -> TestRerun {
    let kind = match element.kind {
        RerunStatusKind::Failure => NonSuccessKind::Failure,
        RerunStatusKind::Error => NonSuccessKind::Error,
    };

    TestRerun {
        kind,
        timestamp: element.data.timestamp,
        time: element.data.time,
        message: element.data.message.clone(),
        ty: element.data.ty.clone(),
        stack_trace: element.data.stack_trace.clone(),
        system_out: element.data.system_out.clone(),
        system_err: element.data.system_err.clone(),
        description: element.data.description.clone(),
    }
}

/// Deserializes properties from XML.
fn deserialize_properties<R: BufRead>(
    reader: &mut Reader<R>,
    path: &[PathElement],
) -> Result<Vec<Property>, DeserializeError> {
    let mut properties = Vec::new();
    let mut buf = Vec::new();
    let mut prop_path = path.to_vec();
    prop_path.push(PathElement::Properties);

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Empty(e)) if e.name().as_ref() == b"property" => {
                let mut elem_path = prop_path.clone();
                elem_path.push(PathElement::Property(properties.len()));
                let property = deserialize_property(&e, &elem_path)?;
                properties.push(property);
            }
            Ok(Event::End(e)) if e.name().as_ref() == b"properties" => break,
            Ok(Event::Eof) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::InvalidStructure(
                        "unexpected EOF in <properties>".to_string(),
                    ),
                    prop_path,
                ))
            }
            Ok(_) => {}
            Err(e) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::XmlError(e),
                    prop_path,
                ))
            }
        }
        buf.clear();
    }

    Ok(properties)
}

/// Deserializes a single property.
fn deserialize_property(
    element: &BytesStart<'_>,
    path: &[PathElement],
) -> Result<Property, DeserializeError> {
    let mut name = None;
    let mut value = None;

    for attr in element.attributes() {
        let attr = attr.map_err(|e| {
            DeserializeError::new(DeserializeErrorKind::AttrError(e), path.to_vec())
        })?;
        let mut attr_path = path.to_vec();
        match attr.key.as_ref() {
            b"name" => {
                attr_path.push(PathElement::Attribute("name".to_string()));
                name = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            b"value" => {
                attr_path.push(PathElement::Attribute("value".to_string()));
                value = Some(parse_xml_string(&attr.value, &attr_path)?);
            }
            _ => {} // Ignore unknown attributes
        }
    }

    let name = name.ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::Attribute("name".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("name".to_string()),
            attr_path,
        )
    })?;
    let value = value.ok_or_else(|| {
        let mut attr_path = path.to_vec();
        attr_path.push(PathElement::Attribute("value".to_string()));
        DeserializeError::new(
            DeserializeErrorKind::MissingAttribute("value".to_string()),
            attr_path,
        )
    })?;

    Ok(Property { name, value })
}

/// Reads text content from an element.
fn read_text_content<R: BufRead>(
    reader: &mut Reader<R>,
    element_name: &[u8],
    path: &[PathElement],
) -> Result<XmlString, DeserializeError> {
    let mut text = String::new();
    let mut buf = Vec::new();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Text(e)) => {
                let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
                })?;
                let unescaped = unescape_with(s, resolve_xml_entity).map_err(|e| {
                    DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec())
                })?;
                text.push_str(&unescaped);
            }
            Ok(Event::CData(e)) => {
                // CDATA sections are already unescaped, just convert to UTF-8.
                let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
                })?;
                text.push_str(s);
            }
            Ok(Event::GeneralRef(e)) => {
                let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
                })?;
                let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
                    DeserializeError::new(
                        DeserializeErrorKind::InvalidStructure(format!(
                            "unrecognized entity: {entity_name}",
                        )),
                        path.to_vec(),
                    )
                })?;
                text.push_str(unescaped);
            }
            Ok(Event::End(e)) if e.name().as_ref() == element_name => break,
            Ok(Event::Eof) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::InvalidStructure(format!(
                        "unexpected EOF in <{}>",
                        String::from_utf8_lossy(element_name)
                    )),
                    path.to_vec(),
                ))
            }
            Ok(_) => {}
            Err(e) => {
                return Err(DeserializeError::new(
                    DeserializeErrorKind::XmlError(e),
                    path.to_vec(),
                ))
            }
        }
        buf.clear();
    }

    // Trim leading and trailing whitespace from the text content.
    Ok(XmlString::new(text.trim()))
}

// ---
// Helper functions
// ---

fn parse_xml_string(bytes: &[u8], path: &[PathElement]) -> Result<XmlString, DeserializeError> {
    let s = std::str::from_utf8(bytes)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
    let unescaped = unescape_with(s, resolve_xml_entity)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec()))?;
    Ok(XmlString::new(unescaped.as_ref()))
}

fn parse_usize(bytes: &[u8], path: &[PathElement]) -> Result<usize, DeserializeError> {
    let s = std::str::from_utf8(bytes)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
    s.parse()
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::ParseIntError(e), path.to_vec()))
}

fn parse_duration(bytes: &[u8], path: &[PathElement]) -> Result<Duration, DeserializeError> {
    let s = std::str::from_utf8(bytes)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
    let seconds: f64 = s.parse().map_err(|_| {
        DeserializeError::new(
            DeserializeErrorKind::ParseDurationError(s.to_string()),
            path.to_vec(),
        )
    })?;

    Duration::try_from_secs_f64(seconds).map_err(|_| {
        DeserializeError::new(
            DeserializeErrorKind::ParseDurationError(s.to_string()),
            path.to_vec(),
        )
    })
}

fn parse_timestamp(
    bytes: &[u8],
    path: &[PathElement],
) -> Result<DateTime<FixedOffset>, DeserializeError> {
    let s = std::str::from_utf8(bytes)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
    DateTime::parse_from_rfc3339(s).map_err(|_| {
        DeserializeError::new(
            DeserializeErrorKind::ParseTimestampError(s.to_string()),
            path.to_vec(),
        )
    })
}

fn parse_uuid(bytes: &[u8], path: &[PathElement]) -> Result<ReportUuid, DeserializeError> {
    let s = std::str::from_utf8(bytes)
        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
    let uuid = s.parse().map_err(|e| {
        DeserializeError::new(DeserializeErrorKind::ParseUuidError(e), path.to_vec())
    })?;
    Ok(ReportUuid::from_untyped_uuid(uuid))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_report() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="my-test-run" tests="1" failures="0" errors="0">
    <testsuite name="my-test-suite" tests="1" disabled="0" errors="0" failures="0">
        <testcase name="success-case"/>
    </testsuite>
</testsuites>
"#;

        let report = Report::deserialize_from_str(xml).unwrap();
        assert_eq!(report.name.as_str(), "my-test-run");
        assert_eq!(report.tests, 1);
        assert_eq!(report.failures, 0);
        assert_eq!(report.errors, 0);
        assert_eq!(report.test_suites.len(), 1);

        let suite = &report.test_suites[0];
        assert_eq!(suite.name.as_str(), "my-test-suite");
        assert_eq!(suite.test_cases.len(), 1);

        let case = &suite.test_cases[0];
        assert_eq!(case.name.as_str(), "success-case");
        assert!(matches!(case.status, TestCaseStatus::Success { .. }));
    }

    #[test]
    fn test_parse_report_with_failure() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="test-run" tests="1" failures="1" errors="0">
    <testsuite name="suite" tests="1" disabled="0" errors="0" failures="1">
        <testcase name="failing-test">
            <failure message="assertion failed">Expected true but got false</failure>
        </testcase>
    </testsuite>
</testsuites>
"#;

        let report = Report::deserialize_from_str(xml).unwrap();
        let case = &report.test_suites[0].test_cases[0];

        match &case.status {
            TestCaseStatus::NonSuccess {
                kind,
                message,
                description,
                ..
            } => {
                assert_eq!(*kind, NonSuccessKind::Failure);
                assert_eq!(message.as_ref().unwrap().as_str(), "assertion failed");
                assert_eq!(
                    description.as_ref().unwrap().as_str(),
                    "Expected true but got false"
                );
            }
            _ => panic!("Expected NonSuccess status"),
        }
    }

    #[test]
    fn test_parse_report_with_properties() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="test-run" tests="1" failures="0" errors="0">
    <testsuite name="suite" tests="1" disabled="0" errors="0" failures="0">
        <properties>
            <property name="env" value="test"/>
            <property name="platform" value="linux"/>
        </properties>
        <testcase name="test"/>
    </testsuite>
</testsuites>
"#;

        let report = Report::deserialize_from_str(xml).unwrap();
        let suite = &report.test_suites[0];

        assert_eq!(suite.properties.len(), 2);
        assert_eq!(suite.properties[0].name.as_str(), "env");
        assert_eq!(suite.properties[0].value.as_str(), "test");
        assert_eq!(suite.properties[1].name.as_str(), "platform");
        assert_eq!(suite.properties[1].value.as_str(), "linux");
    }
}
