//! Tagged PDF structure tree and accessibility support (PDF/UA)
//!
//! Implements parsing of /StructTreeRoot, structure elements, role mapping,
//! reading order traversal, heading validation, and PDF/UA-1 validation.

use crate::fitz::error::{Error, Result};
use std::collections::HashMap;

use super::document::PdfDocument;
use super::object::{Dict, Name, ObjRef, Object};

/// Standard structure roles from ISO 32000-2 (PDF 2.0) and PDF/UA.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StructureRole {
    Document,
    Part,
    Art,
    Sect,
    Div,
    BlockQuote,
    Caption,
    TOC,
    TOCI,
    Index,
    NonStruct,
    Private,
    P,
    H,
    H1,
    H2,
    H3,
    H4,
    H5,
    H6,
    L,
    LI,
    Lbl,
    LBody,
    Table,
    TR,
    TH,
    TD,
    THead,
    TBody,
    TFoot,
    Span,
    Quote,
    Note,
    Reference,
    BibEntry,
    Code,
    Link,
    Annot,
    Ruby,
    Warichu,
    Figure,
    Formula,
    Form,
    /// Custom role not in standard set (mapped via RoleMap or unknown)
    Custom(String),
}

impl StructureRole {
    /// Standard PDF role name string.
    pub fn as_str(&self) -> &str {
        match self {
            StructureRole::Document => "Document",
            StructureRole::Part => "Part",
            StructureRole::Art => "Art",
            StructureRole::Sect => "Sect",
            StructureRole::Div => "Div",
            StructureRole::BlockQuote => "BlockQuote",
            StructureRole::Caption => "Caption",
            StructureRole::TOC => "TOC",
            StructureRole::TOCI => "TOCI",
            StructureRole::Index => "Index",
            StructureRole::NonStruct => "NonStruct",
            StructureRole::Private => "Private",
            StructureRole::P => "P",
            StructureRole::H => "H",
            StructureRole::H1 => "H1",
            StructureRole::H2 => "H2",
            StructureRole::H3 => "H3",
            StructureRole::H4 => "H4",
            StructureRole::H5 => "H5",
            StructureRole::H6 => "H6",
            StructureRole::L => "L",
            StructureRole::LI => "LI",
            StructureRole::Lbl => "Lbl",
            StructureRole::LBody => "LBody",
            StructureRole::Table => "Table",
            StructureRole::TR => "TR",
            StructureRole::TH => "TH",
            StructureRole::TD => "TD",
            StructureRole::THead => "THead",
            StructureRole::TBody => "TBody",
            StructureRole::TFoot => "TFoot",
            StructureRole::Span => "Span",
            StructureRole::Quote => "Quote",
            StructureRole::Note => "Note",
            StructureRole::Reference => "Reference",
            StructureRole::BibEntry => "BibEntry",
            StructureRole::Code => "Code",
            StructureRole::Link => "Link",
            StructureRole::Annot => "Annot",
            StructureRole::Ruby => "Ruby",
            StructureRole::Warichu => "Warichu",
            StructureRole::Figure => "Figure",
            StructureRole::Formula => "Formula",
            StructureRole::Form => "Form",
            StructureRole::Custom(s) => s.as_str(),
        }
    }

    /// Parse from PDF role name.
    pub fn from_str(s: &str) -> Self {
        match s {
            "Document" => StructureRole::Document,
            "Part" => StructureRole::Part,
            "Art" => StructureRole::Art,
            "Sect" => StructureRole::Sect,
            "Div" => StructureRole::Div,
            "BlockQuote" => StructureRole::BlockQuote,
            "Caption" => StructureRole::Caption,
            "TOC" => StructureRole::TOC,
            "TOCI" => StructureRole::TOCI,
            "Index" => StructureRole::Index,
            "NonStruct" => StructureRole::NonStruct,
            "Private" => StructureRole::Private,
            "P" => StructureRole::P,
            "H" => StructureRole::H,
            "H1" => StructureRole::H1,
            "H2" => StructureRole::H2,
            "H3" => StructureRole::H3,
            "H4" => StructureRole::H4,
            "H5" => StructureRole::H5,
            "H6" => StructureRole::H6,
            "L" => StructureRole::L,
            "LI" => StructureRole::LI,
            "Lbl" => StructureRole::Lbl,
            "LBody" => StructureRole::LBody,
            "Table" => StructureRole::Table,
            "TR" => StructureRole::TR,
            "TH" => StructureRole::TH,
            "TD" => StructureRole::TD,
            "THead" => StructureRole::THead,
            "TBody" => StructureRole::TBody,
            "TFoot" => StructureRole::TFoot,
            "Span" => StructureRole::Span,
            "Quote" => StructureRole::Quote,
            "Note" => StructureRole::Note,
            "Reference" => StructureRole::Reference,
            "BibEntry" => StructureRole::BibEntry,
            "Code" => StructureRole::Code,
            "Link" => StructureRole::Link,
            "Annot" => StructureRole::Annot,
            "Ruby" => StructureRole::Ruby,
            "Warichu" => StructureRole::Warichu,
            "Figure" => StructureRole::Figure,
            "Formula" => StructureRole::Formula,
            "Form" => StructureRole::Form,
            other => StructureRole::Custom(other.to_string()),
        }
    }

    /// Whether this role is a heading (H, H1–H6).
    pub fn is_heading(&self) -> bool {
        matches!(
            self,
            StructureRole::H
                | StructureRole::H1
                | StructureRole::H2
                | StructureRole::H3
                | StructureRole::H4
                | StructureRole::H5
                | StructureRole::H6
        )
    }

    /// Heading level (1–6) if this is a heading, else None.
    pub fn heading_level(&self) -> Option<u8> {
        match self {
            StructureRole::H | StructureRole::H1 => Some(1),
            StructureRole::H2 => Some(2),
            StructureRole::H3 => Some(3),
            StructureRole::H4 => Some(4),
            StructureRole::H5 => Some(5),
            StructureRole::H6 => Some(6),
            _ => None,
        }
    }

    /// Whether this role is Figure (needs alt text for PDF/UA).
    pub fn is_figure(&self) -> bool {
        matches!(self, StructureRole::Figure)
    }
}

/// A structure element (StructElem) in the logical structure tree.
#[derive(Debug, Clone)]
pub struct StructureElement {
    pub role: StructureRole,
    pub title: Option<String>,
    pub alt_text: Option<String>,
    pub actual_text: Option<String>,
    pub lang: Option<String>,
    pub children: Vec<StructureNode>,
    pub attributes: HashMap<String, Object>,
    pub parent_index: Option<usize>,
    pub mcid: Option<i32>,
}

impl StructureElement {
    pub fn new(role: StructureRole) -> Self {
        Self {
            role,
            title: None,
            alt_text: None,
            actual_text: None,
            lang: None,
            children: Vec::new(),
            attributes: HashMap::new(),
            parent_index: None,
            mcid: None,
        }
    }
}

/// A node in the structure tree: element, marked content, or object reference.
#[derive(Debug, Clone)]
pub enum StructureNode {
    Element(StructureElement),
    MarkedContent(i32),
    ObjectReference(ObjRef),
}

/// Parsed structure tree from /StructTreeRoot.
#[derive(Debug, Clone, Default)]
pub struct StructureTree {
    pub root_elements: Vec<StructureElement>,
    pub role_map: HashMap<String, StructureRole>,
    pub class_map: HashMap<String, Dict>,
}

impl StructureTree {
    pub fn new() -> Self {
        Self::default()
    }

    /// Resolve a role name through RoleMap to standard role.
    pub fn resolve_role(&self, name: &str) -> StructureRole {
        self.role_map
            .get(name)
            .cloned()
            .unwrap_or_else(|| StructureRole::from_str(name))
    }
}

/// Parse /StructTreeRoot from catalog.
///
/// Expects `doc` to have resolved objects (use `doc.resolve()` on catalog).
pub fn parse_structure_tree(
    doc: &impl StructureTreeDocument,
    catalog: &Dict,
) -> Result<Option<StructureTree>> {
    let struct_tree_root = catalog.get(&Name::new("StructTreeRoot"));
    let root_dict = match struct_tree_root {
        Some(Object::Ref(r)) => {
            let resolved = doc.resolve_ref(r.num, r.generation)?;
            resolved
                .as_dict()
                .ok_or_else(|| {
                    Error::Pdf("StructTreeRoot reference does not resolve to dictionary".into())
                })?
                .clone()
        }
        Some(Object::Dict(d)) => d.clone(),
        _ => return Ok(None),
    };

    let resolved_root = doc.resolve_dict(&root_dict)?;

    let mut tree = StructureTree::new();

    // Parse RoleMap: custom role -> standard role
    if let Some(role_map_obj) = resolved_root.get(&Name::new("RoleMap")) {
        let role_map_dict = doc.resolve_to_dict(role_map_obj)?;
        for (k, v) in role_map_dict.iter() {
            let custom = k.as_str();
            let standard = v.as_name().map(|n| n.as_str()).unwrap_or("");
            tree.role_map
                .insert(custom.to_string(), StructureRole::from_str(standard));
        }
    }

    // Parse ClassMap: class name -> attribute dict
    if let Some(class_map_obj) = resolved_root.get(&Name::new("ClassMap")) {
        let class_map_dict = doc.resolve_to_dict(class_map_obj)?;
        for (k, v) in class_map_dict.iter() {
            let class_name = k.as_str().to_string();
            if let Some(d) = v.as_dict() {
                tree.class_map.insert(class_name, d.clone());
            }
        }
    }

    // Parse K (children) array recursively
    if let Some(k_obj) = resolved_root.get(&Name::new("K")) {
        let children = parse_k_children(doc, k_obj, &tree, None)?;
        for node in children {
            if let StructureNode::Element(el) = node {
                tree.root_elements.push(el);
            }
        }
    }

    Ok(Some(tree))
}

/// Trait for document types that can provide structure tree parsing.
pub trait StructureTreeDocument {
    fn resolve_ref(&self, num: i32, generation: i32) -> Result<Object>;
    fn resolve_dict(&self, dict: &Dict) -> Result<Dict>;
    fn resolve_to_dict(&self, obj: &Object) -> Result<Dict>;
}

fn parse_k_children(
    doc: &impl StructureTreeDocument,
    k_obj: &Object,
    tree: &StructureTree,
    parent_index: Option<usize>,
) -> Result<Vec<StructureNode>> {
    let mut nodes = Vec::new();

    let resolved = match k_obj {
        Object::Ref(r) => doc.resolve_ref(r.num, r.generation)?,
        other => other.clone(),
    };

    match &resolved {
        Object::Array(arr) => {
            for item in arr.iter() {
                let resolved_item = match item {
                    Object::Ref(r) => doc.resolve_ref(r.num, r.generation)?,
                    other => other.clone(),
                };
                let parsed = parse_k_item(doc, &resolved_item, tree, parent_index)?;
                nodes.extend(parsed);
            }
        }
        single => {
            let parsed = parse_k_item(doc, single, tree, parent_index)?;
            nodes.extend(parsed);
        }
        _ => {}
    }

    Ok(nodes)
}

fn parse_k_item(
    doc: &impl StructureTreeDocument,
    obj: &Object,
    tree: &StructureTree,
    parent_index: Option<usize>,
) -> Result<Vec<StructureNode>> {
    let dict = match obj {
        Object::Dict(d) => d,
        Object::Ref(r) => {
            let resolved = doc.resolve_ref(r.num, r.generation)?;
            return match &resolved {
                Object::Dict(d) => parse_k_item(doc, &resolved, tree, parent_index),
                Object::Int(i) => Ok(vec![StructureNode::MarkedContent(*i as i32)]),
                _ => Ok(vec![]),
            };
        }
        Object::Int(i) => return Ok(vec![StructureNode::MarkedContent(*i as i32)]),
        _ => return Ok(vec![]),
    };

    let type_name = dict
        .get(&Name::new("Type"))
        .and_then(|o| o.as_name())
        .map(|n| n.as_str())
        .unwrap_or("");

    if type_name == "StructElem" {
        let elem = parse_struct_elem(doc, dict, tree, parent_index)?;
        return Ok(vec![StructureNode::Element(elem)]);
    }

    if type_name == "OBJR" {
        if let Some(ref_obj) = dict.get(&Name::new("Obj")) {
            if let Object::Ref(r) = ref_obj {
                return Ok(vec![StructureNode::ObjectReference(ObjRef {
                    num: r.num,
                    generation: r.generation,
                })]);
            }
        }
    }

    Ok(vec![])
}

/// Parse a single /StructElem dictionary.
pub fn parse_struct_elem(
    doc: &impl StructureTreeDocument,
    dict: &Dict,
    tree: &StructureTree,
    parent_index: Option<usize>,
) -> Result<StructureElement> {
    let role_name = dict
        .get(&Name::new("S"))
        .and_then(|o| o.as_name())
        .map(|n| n.as_str())
        .ok_or_else(|| Error::Pdf("StructElem missing /S (role)".into()))?;

    let role = tree.resolve_role(role_name);

    let title = dict
        .get(&Name::new("T"))
        .and_then(|o| extract_string(o, doc));

    let alt_text = dict
        .get(&Name::new("Alt"))
        .and_then(|o| extract_string(o, doc));

    let actual_text = dict
        .get(&Name::new("ActualText"))
        .and_then(|o| extract_string(o, doc));

    let lang = dict
        .get(&Name::new("Lang"))
        .and_then(|o| extract_string(o, doc));

    let mcid = dict
        .get(&Name::new("MCID"))
        .and_then(|o| o.as_int())
        .map(|i| i as i32);

    let mut attributes: HashMap<String, Object> = HashMap::new();
    if let Some(a) = dict.get(&Name::new("A")) {
        if let Some(d) = a.as_dict() {
            for (k, v) in d.iter() {
                attributes.insert(k.as_str().to_string(), v.clone());
            }
        }
    }

    let mut children = Vec::new();
    if let Some(k_obj) = dict.get(&Name::new("K")) {
        let resolved = match k_obj {
            Object::Ref(r) => doc.resolve_ref(r.num, r.generation)?,
            other => other.clone(),
        };
        let child_nodes = parse_k_children(doc, &resolved, tree, None)?;
        children = child_nodes;
    }

    Ok(StructureElement {
        role,
        title,
        alt_text,
        actual_text,
        lang,
        children,
        attributes,
        parent_index,
        mcid,
    })
}

fn extract_string(obj: &Object, doc: &impl StructureTreeDocument) -> Option<String> {
    match obj {
        Object::String(s) => s.as_str().map(String::from),
        Object::Ref(r) => {
            let resolved = doc.resolve_ref(r.num, r.generation).ok()?;
            match &resolved {
                Object::String(s) => s.as_str().map(String::from),
                _ => None,
            }
        }
        _ => None,
    }
}

/// Traverse structure tree in document order to produce flat reading order list.
pub fn get_reading_order(tree: &StructureTree) -> Vec<&StructureElement> {
    let mut order = Vec::new();
    for root in &tree.root_elements {
        collect_reading_order(root, &mut order);
    }
    order
}

fn collect_reading_order<'a>(elem: &'a StructureElement, out: &mut Vec<&'a StructureElement>) {
    out.push(elem);
    for child in &elem.children {
        if let StructureNode::Element(el) = child {
            collect_reading_order(el, out);
        }
    }
}

/// Validate that headings (H1–H6) are properly nested (no skipped levels).
pub fn validate_heading_hierarchy(tree: &StructureTree) -> Vec<String> {
    let mut errors = Vec::new();
    let mut stack: Vec<u8> = Vec::new();

    for elem in get_reading_order(tree) {
        if let Some(level) = elem.role.heading_level() {
            while stack.last().copied().unwrap_or(0) >= level {
                stack.pop();
            }
            if let Some(prev) = stack.last() {
                if level > prev + 1 {
                    errors.push(format!(
                        "Heading level {} follows level {} (skipped level)",
                        level, prev
                    ));
                }
            }
            stack.push(level);
        }
    }

    errors
}

/// Extract alternative text for a structure element (for images/figures).
pub fn get_alt_text(elem: &StructureElement) -> Option<&str> {
    elem.alt_text.as_deref().or(elem.actual_text.as_deref())
}

/// Basic PDF/UA-1 validation results.
#[derive(Debug, Clone, Default)]
pub struct PdfUaValidationResult {
    pub valid: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
}

/// Basic PDF/UA-1 validator.
pub struct PdfUaValidator;

impl PdfUaValidator {
    /// Run PDF/UA-1 validation checks.
    pub fn validate(doc: &(impl PdfUaDocument + StructureTreeDocument)) -> PdfUaValidationResult {
        let mut result = PdfUaValidationResult {
            valid: true,
            errors: Vec::new(),
            warnings: Vec::new(),
        };

        // Document must have /StructTreeRoot
        let catalog = match doc.catalog() {
            Ok(c) => c,
            Err(_) => {
                result.errors.push("Document has no catalog".to_string());
                result.valid = false;
                return result;
            }
        };

        if catalog.get(&Name::new("StructTreeRoot")).is_none() {
            result
                .errors
                .push("Document must have /StructTreeRoot for PDF/UA".to_string());
            result.valid = false;
        }

        // Parse structure tree and run further checks
        if let Ok(Some(tree)) = parse_structure_tree(doc, catalog) {
            // Figures must have alt text
            for elem in get_reading_order(&tree) {
                if elem.role.is_figure() {
                    if get_alt_text(elem)
                        .map(|s| s.trim().is_empty())
                        .unwrap_or(true)
                    {
                        result.errors.push(format!(
                            "Figure element must have /Alt or /ActualText for PDF/UA"
                        ));
                        result.valid = false;
                    }
                }
            }

            // Headings must be properly nested
            let heading_errors = validate_heading_hierarchy(&tree);
            for e in heading_errors {
                result.errors.push(e);
                result.valid = false;
            }
        }

        // Document language must be set (in catalog or mark info)
        if doc.document_lang().is_none() {
            result
                .warnings
                .push("Document language (/Lang) should be set for PDF/UA".to_string());
        }

        result
    }
}

/// Trait for document types that can be PDF/UA validated.
pub trait PdfUaDocument {
    fn catalog(&self) -> Result<&Dict>;
    fn document_lang(&self) -> Option<String>;
}

impl StructureTreeDocument for PdfDocument {
    fn resolve_ref(&self, num: i32, generation: i32) -> Result<Object> {
        let obj = self
            .get_object(num, generation)
            .ok_or_else(|| Error::Pdf(format!("Object {} {} not found", num, generation)))?;
        Ok(self.resolve(obj))
    }

    fn resolve_dict(&self, dict: &Dict) -> Result<Dict> {
        let mut resolved = Dict::new();
        for (k, v) in dict.iter() {
            resolved.insert(k.clone(), self.resolve(v));
        }
        Ok(resolved)
    }

    fn resolve_to_dict(&self, obj: &Object) -> Result<Dict> {
        let resolved = self.resolve(obj);
        resolved
            .as_dict()
            .cloned()
            .ok_or_else(|| Error::Pdf("Expected dictionary".into()))
    }
}

impl PdfUaDocument for PdfDocument {
    fn catalog(&self) -> Result<&Dict> {
        PdfDocument::catalog(self)
    }

    fn document_lang(&self) -> Option<String> {
        let catalog = PdfDocument::catalog(self).ok()?;
        if let Some(lang_obj) = catalog.get(&Name::new("Lang")) {
            let resolved = self.resolve(lang_obj);
            if let Some(s) = resolved.as_string() {
                return s.as_str().map(String::from);
            }
        }
        if let Some(mark_info) = catalog.get(&Name::new("MarkInfo")) {
            let resolved = self.resolve(mark_info);
            if let Some(d) = resolved.as_dict() {
                if let Some(lang_obj) = d.get(&Name::new("Lang")) {
                    let lang_resolved = self.resolve(lang_obj);
                    if let Some(s) = lang_resolved.as_string() {
                        return s.as_str().map(String::from);
                    }
                }
            }
        }
        None
    }
}

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

    #[test]
    fn test_structure_role_from_str() {
        assert_eq!(StructureRole::from_str("Document").as_str(), "Document");
        assert_eq!(StructureRole::from_str("P").as_str(), "P");
        assert_eq!(StructureRole::from_str("H1").as_str(), "H1");
        assert_eq!(StructureRole::from_str("Figure").as_str(), "Figure");
        assert_eq!(StructureRole::from_str("CustomRole").as_str(), "CustomRole");
    }

    #[test]
    fn test_structure_role_heading() {
        assert!(StructureRole::H1.is_heading());
        assert!(StructureRole::H6.is_heading());
        assert!(!StructureRole::P.is_heading());
        assert!(!StructureRole::Figure.is_heading());
        assert_eq!(StructureRole::H1.heading_level(), Some(1));
        assert_eq!(StructureRole::H3.heading_level(), Some(3));
        assert_eq!(StructureRole::P.heading_level(), None);
    }

    #[test]
    fn test_structure_role_figure() {
        assert!(StructureRole::Figure.is_figure());
        assert!(!StructureRole::P.is_figure());
    }

    #[test]
    fn test_structure_tree_role_map() {
        let mut tree = StructureTree::new();
        tree.role_map
            .insert("MyHeading".to_string(), StructureRole::H1);
        tree.role_map.insert("MyPara".to_string(), StructureRole::P);

        assert_eq!(tree.resolve_role("MyHeading").as_str(), "H1");
        assert_eq!(tree.resolve_role("MyPara").as_str(), "P");
        assert_eq!(tree.resolve_role("Unknown").as_str(), "Unknown");
    }

    #[test]
    fn test_get_reading_order() {
        let mut root = StructureElement::new(StructureRole::Document);
        let mut sect = StructureElement::new(StructureRole::Sect);
        let p1 = StructureElement::new(StructureRole::P);
        let p2 = StructureElement::new(StructureRole::P);
        sect.children.push(StructureNode::Element(p1));
        sect.children.push(StructureNode::Element(p2));
        root.children.push(StructureNode::Element(sect));

        let tree = StructureTree {
            root_elements: vec![root],
            role_map: HashMap::new(),
            class_map: HashMap::new(),
        };

        let order = get_reading_order(&tree);
        assert_eq!(order.len(), 4);
        assert_eq!(order[0].role.as_str(), "Document");
        assert_eq!(order[1].role.as_str(), "Sect");
        assert_eq!(order[2].role.as_str(), "P");
        assert_eq!(order[3].role.as_str(), "P");
    }

    #[test]
    fn test_validate_heading_hierarchy_valid() {
        let mut doc = StructureElement::new(StructureRole::Document);
        let h1 = StructureElement::new(StructureRole::H1);
        let h2 = StructureElement::new(StructureRole::H2);
        let h3 = StructureElement::new(StructureRole::H3);
        doc.children.push(StructureNode::Element(h1));
        doc.children.push(StructureNode::Element(h2));
        doc.children.push(StructureNode::Element(h3));

        let tree = StructureTree {
            root_elements: vec![doc],
            role_map: HashMap::new(),
            class_map: HashMap::new(),
        };

        let errors = validate_heading_hierarchy(&tree);
        assert!(errors.is_empty());
    }

    #[test]
    fn test_validate_heading_hierarchy_skipped_level() {
        let mut doc = StructureElement::new(StructureRole::Document);
        let h1 = StructureElement::new(StructureRole::H1);
        let h3 = StructureElement::new(StructureRole::H3);
        doc.children.push(StructureNode::Element(h1));
        doc.children.push(StructureNode::Element(h3));

        let tree = StructureTree {
            root_elements: vec![doc],
            role_map: HashMap::new(),
            class_map: HashMap::new(),
        };

        let errors = validate_heading_hierarchy(&tree);
        assert_eq!(errors.len(), 1);
        assert!(errors[0].contains("skipped level"));
    }

    #[test]
    fn test_get_alt_text() {
        let mut elem = StructureElement::new(StructureRole::Figure);
        assert!(get_alt_text(&elem).is_none());

        elem.alt_text = Some("Image of a chart".to_string());
        assert_eq!(get_alt_text(&elem), Some("Image of a chart"));

        elem.alt_text = None;
        elem.actual_text = Some("Actual text".to_string());
        assert_eq!(get_alt_text(&elem), Some("Actual text"));
    }

    #[test]
    fn test_pdf_ua_validator_no_struct_tree() {
        let pdf = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
xref
0 4
0000000000 65535 f 
0000000009 00000 n 
0000000058 00000 n 
0000000115 00000 n 
trailer << /Size 4 /Root 1 0 R /Info 4 0 R >>
startxref
162
%%EOF";
        let doc = PdfDocument::from_bytes(pdf).unwrap();
        let result = PdfUaValidator::validate(&doc);
        assert!(!result.valid);
        assert!(result.errors.iter().any(|e| e.contains("StructTreeRoot")));
    }

    #[test]
    fn test_pdf_ua_validator_with_lang() {
        use crate::pdf::object::PdfString;
        let mut doc = PdfDocument::new();
        let mut catalog = Dict::new();
        catalog.insert(Name::new("Type"), Object::Name(Name::new("Catalog")));
        catalog.insert(Name::new("Pages"), Object::Ref(ObjRef::new(2, 0)));
        catalog.insert(
            Name::new("Lang"),
            Object::String(PdfString::new(b"en-US".to_vec())),
        );
        catalog.insert(Name::new("StructTreeRoot"), Object::Ref(ObjRef::new(4, 0)));
        doc.objects.insert((1, 0), Object::Dict(catalog));

        let mut struct_root = Dict::new();
        struct_root.insert(Name::new("Type"), Object::Name(Name::new("StructTreeRoot")));
        struct_root.insert(Name::new("K"), Object::Ref(ObjRef::new(5, 0)));
        doc.objects.insert((4, 0), Object::Dict(struct_root));

        let mut struct_elem = Dict::new();
        struct_elem.insert(Name::new("Type"), Object::Name(Name::new("StructElem")));
        struct_elem.insert(Name::new("S"), Object::Name(Name::new("Document")));
        struct_elem.insert(Name::new("K"), Object::Array(vec![]));
        doc.objects.insert((5, 0), Object::Dict(struct_elem));

        let mut pages = Dict::new();
        pages.insert(Name::new("Type"), Object::Name(Name::new("Pages")));
        pages.insert(Name::new("Count"), Object::Int(1));
        pages.insert(
            Name::new("Kids"),
            Object::Array(vec![Object::Ref(ObjRef::new(3, 0))]),
        );
        doc.objects.insert((2, 0), Object::Dict(pages));

        let mut page = Dict::new();
        page.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        page.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
        doc.objects.insert((3, 0), Object::Dict(page));

        doc.trailer
            .insert(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));

        let result = PdfUaValidator::validate(&doc);
        assert!(result.valid);
        assert!(doc.document_lang().unwrap_or_default().contains("en"));
    }

    #[test]
    fn test_parse_structure_tree_empty_catalog() {
        let pdf = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
xref
0 4
0000000000 65535 f 
0000000009 00000 n 
0000000058 00000 n 
0000000115 00000 n 
trailer << /Size 4 /Root 1 0 R /Info 4 0 R >>
startxref
162
%%EOF";
        let doc = PdfDocument::from_bytes(pdf).unwrap();
        let catalog = doc.catalog().unwrap();
        let tree = parse_structure_tree(&doc, catalog).unwrap();
        assert!(tree.is_none());
    }
}
