//! PDF annotation type definitions and appearance generation
//!
//! Provides type definitions, parsing helpers, and content stream generation
//! for all 28 PDF annotation types per the PDF specification.

use crate::fitz::error::{Error, Result};
use crate::pdf::object::{Dict, Name, Object};
use std::collections::HashMap;

/// Bézier circle approximation constant: `4 * (sqrt(2) - 1) / 3`.
/// Used for approximating a circle with four cubic Bézier curves.
const BEZIER_CIRCLE_MAGIC: f64 = 0.5522847498307933;

// ============================================================================
// Annotation Type
// ============================================================================

/// All PDF annotation types per the PDF specification
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnotationType {
    Text,
    Link,
    FreeText,
    Line,
    Square,
    Circle,
    Polygon,
    PolyLine,
    Highlight,
    Underline,
    Squiggly,
    StrikeOut,
    Stamp,
    Ink,
    Caret,
    Popup,
    FileAttachment,
    Sound,
    Movie,
    Screen,
    Widget,
    PrinterMark,
    TrapNet,
    Watermark,
    ThreeD,
    Redact,
    Projection,
    RichMedia,
}

impl AnnotationType {
    /// Parse annotation subtype name to type. Returns None for unknown types.
    pub fn from_name(name: &str) -> Option<Self> {
        match name {
            "Text" => Some(Self::Text),
            "Link" => Some(Self::Link),
            "FreeText" => Some(Self::FreeText),
            "Line" => Some(Self::Line),
            "Square" => Some(Self::Square),
            "Circle" => Some(Self::Circle),
            "Polygon" => Some(Self::Polygon),
            "PolyLine" => Some(Self::PolyLine),
            "Highlight" => Some(Self::Highlight),
            "Underline" => Some(Self::Underline),
            "Squiggly" => Some(Self::Squiggly),
            "StrikeOut" => Some(Self::StrikeOut),
            "Stamp" => Some(Self::Stamp),
            "Ink" => Some(Self::Ink),
            "Caret" => Some(Self::Caret),
            "Popup" => Some(Self::Popup),
            "FileAttachment" => Some(Self::FileAttachment),
            "Sound" => Some(Self::Sound),
            "Movie" => Some(Self::Movie),
            "Screen" => Some(Self::Screen),
            "Widget" => Some(Self::Widget),
            "PrinterMark" => Some(Self::PrinterMark),
            "TrapNet" => Some(Self::TrapNet),
            "Watermark" => Some(Self::Watermark),
            "3D" => Some(Self::ThreeD),
            "Redact" => Some(Self::Redact),
            "Projection" => Some(Self::Projection),
            "RichMedia" => Some(Self::RichMedia),
            _ => None,
        }
    }

    /// Get the PDF name string for this annotation type
    pub fn name(&self) -> &'static str {
        match self {
            Self::Text => "Text",
            Self::Link => "Link",
            Self::FreeText => "FreeText",
            Self::Line => "Line",
            Self::Square => "Square",
            Self::Circle => "Circle",
            Self::Polygon => "Polygon",
            Self::PolyLine => "PolyLine",
            Self::Highlight => "Highlight",
            Self::Underline => "Underline",
            Self::Squiggly => "Squiggly",
            Self::StrikeOut => "StrikeOut",
            Self::Stamp => "Stamp",
            Self::Ink => "Ink",
            Self::Caret => "Caret",
            Self::Popup => "Popup",
            Self::FileAttachment => "FileAttachment",
            Self::Sound => "Sound",
            Self::Movie => "Movie",
            Self::Screen => "Screen",
            Self::Widget => "Widget",
            Self::PrinterMark => "PrinterMark",
            Self::TrapNet => "TrapNet",
            Self::Watermark => "Watermark",
            Self::ThreeD => "3D",
            Self::Redact => "Redact",
            Self::Projection => "Projection",
            Self::RichMedia => "RichMedia",
        }
    }

    /// Whether this is a markup annotation (has popup, contents, etc.)
    /// Widget, PrinterMark, and TrapNet are NOT markup annotations per PDF spec.
    pub fn is_markup(&self) -> bool {
        matches!(
            self,
            Self::Text
                | Self::FreeText
                | Self::Line
                | Self::Square
                | Self::Circle
                | Self::Polygon
                | Self::PolyLine
                | Self::Highlight
                | Self::Underline
                | Self::Squiggly
                | Self::StrikeOut
                | Self::Stamp
                | Self::Ink
                | Self::Caret
                | Self::FileAttachment
                | Self::Sound
                | Self::Movie
                | Self::Screen
                | Self::Watermark
                | Self::ThreeD
                | Self::Redact
                | Self::Projection
                | Self::RichMedia
        )
    }

    /// Default icon for icon-based annotations (Text, FileAttachment, Sound)
    pub fn default_icon(&self) -> Option<AnnotationIcon> {
        match self {
            Self::Text => Some(AnnotationIcon::Note),
            Self::FileAttachment => Some(AnnotationIcon::PaperclipTag),
            Self::Sound => Some(AnnotationIcon::Speaker),
            _ => None,
        }
    }
}

// ============================================================================
// Annotation Flags
// ============================================================================

/// Annotation flag constants (bitfield)
#[allow(non_snake_case)]
pub mod AnnotationFlags {
    pub const INVISIBLE: u32 = 1 << 0;
    pub const HIDDEN: u32 = 1 << 1;
    pub const PRINT: u32 = 1 << 2;
    pub const NO_ZOOM: u32 = 1 << 3;
    pub const NO_ROTATE: u32 = 1 << 4;
    pub const NO_VIEW: u32 = 1 << 5;
    pub const READ_ONLY: u32 = 1 << 6;
    pub const LOCKED: u32 = 1 << 7;
    pub const TOGGLE_NO_VIEW: u32 = 1 << 8;
    pub const LOCKED_CONTENTS: u32 = 1 << 9;
}

// ============================================================================
// Annotation Appearance
// ============================================================================

/// Parsed annotation appearance streams
#[derive(Debug, Clone, Default)]
pub struct AnnotationAppearance {
    /// /N appearance stream bytes
    pub normal: Option<Vec<u8>>,
    /// /R appearance stream bytes
    pub rollover: Option<Vec<u8>>,
    /// /D appearance stream bytes
    pub down: Option<Vec<u8>>,
    /// Bounding box [llx, lly, urx, ury]
    pub bbox: [f64; 4],
}

// ============================================================================
// Annotation Icon
// ============================================================================

/// Standard icon names for icon-based annotations
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnotationIcon {
    /// Text annotation icons
    Comment,
    Key,
    Note,
    Help,
    NewParagraph,
    Paragraph,
    Insert,
    /// FileAttachment icons
    GraphPushPin,
    PaperclipTag,
    /// Sound icon
    Speaker,
}

impl AnnotationIcon {
    pub fn name(&self) -> &'static str {
        match self {
            Self::Comment => "Comment",
            Self::Key => "Key",
            Self::Note => "Note",
            Self::Help => "Help",
            Self::NewParagraph => "NewParagraph",
            Self::Paragraph => "Paragraph",
            Self::Insert => "Insert",
            Self::GraphPushPin => "GraphPushPin",
            Self::PaperclipTag => "PaperclipTag",
            Self::Speaker => "Speaker",
        }
    }
}

// ============================================================================
// Border Style
// ============================================================================

/// PDF border style (S, D, B, I, U)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorderStyle {
    Solid,
    Dashed,
    Beveled,
    Inset,
    Underline,
}

/// Parsed annotation border style
#[derive(Debug, Clone)]
pub struct AnnotationBorderStyle {
    pub width: f64,
    pub style: BorderStyle,
    pub dash_pattern: Vec<f64>,
}

impl Default for AnnotationBorderStyle {
    fn default() -> Self {
        Self {
            width: 1.0,
            style: BorderStyle::Solid,
            dash_pattern: Vec::new(),
        }
    }
}

// ============================================================================
// Parsing Functions
// ============================================================================

/// Extract numeric value from Object, handling both Int and Real per PDF spec.
/// Returns None for non-finite values (NaN, Infinity) to avoid invalid geometry.
fn get_number(obj: &Object) -> Option<f64> {
    let v = match obj {
        Object::Int(i) => *i as f64,
        Object::Real(r) => *r,
        _ => return None,
    };
    if v.is_finite() { Some(v) } else { None }
}

/// Parse annotation type, rect, and flags from dictionary
pub fn parse_annotation(dict: &Dict) -> Result<(AnnotationType, [f64; 4], u32)> {
    let subtype = dict
        .get(&Name::new("Subtype"))
        .and_then(|o| o.as_name())
        .map(|n| n.as_str())
        .ok_or_else(|| Error::pdf("Annotation missing Subtype"))?;

    let annot_type = AnnotationType::from_name(subtype)
        .ok_or_else(|| Error::pdf(format!("Unknown annotation subtype: {}", subtype)))?;

    let mut rect =
        parse_rect(dict).ok_or_else(|| Error::pdf("Annotation missing required Rect"))?;
    // Normalize so llx < urx and lly < ury per PDF spec
    let llx = rect.get(0).copied().unwrap_or(0.0);
    let lly = rect.get(1).copied().unwrap_or(0.0);
    let urx = rect.get(2).copied().unwrap_or(0.0);
    let ury = rect.get(3).copied().unwrap_or(0.0);
    rect = [llx.min(urx), lly.min(ury), llx.max(urx), lly.max(ury)];

    let flags = dict
        .get(&Name::new("F"))
        .and_then(|o| o.as_int())
        .and_then(|i| i.try_into().ok())
        .unwrap_or(0);

    Ok((annot_type, rect, flags))
}

fn parse_rect(dict: &Dict) -> Option<[f64; 4]> {
    let arr = dict.get(&Name::new("Rect")).and_then(|o| o.as_array())?;
    if arr.len() < 4 {
        return None;
    }
    let llx = arr.get(0).and_then(get_number)?;
    let lly = arr.get(1).and_then(get_number)?;
    let urx = arr.get(2).and_then(get_number)?;
    let ury = arr.get(3).and_then(get_number)?;
    Some([llx, lly, urx, ury])
}

/// Parse /BS or /Border entries for border style.
///
/// Uses lenient parsing with defaults: if get_number returns None (e.g. NaN/Infinity),
/// unwrap_or(1.0) silently uses the default. Rect parsing is strict and rejects
/// non-finite values.
pub fn parse_border_style(dict: &Dict) -> AnnotationBorderStyle {
    if let Some(bs) = dict.get(&Name::new("BS")).and_then(|o| o.as_dict()) {
        let width = bs.get(&Name::new("W")).and_then(get_number).unwrap_or(1.0);
        let style = bs
            .get(&Name::new("S"))
            .and_then(|o| o.as_name())
            .map(|n| match n.as_str() {
                "D" => BorderStyle::Dashed,
                "B" => BorderStyle::Beveled,
                "I" => BorderStyle::Inset,
                "U" => BorderStyle::Underline,
                _ => BorderStyle::Solid,
            })
            .unwrap_or(BorderStyle::Solid);
        let dash_pattern = bs
            .get(&Name::new("D"))
            .and_then(|o| o.as_array())
            .map(|a| a.iter().filter_map(get_number).collect::<Vec<f64>>())
            .unwrap_or_default();
        return AnnotationBorderStyle {
            width,
            style,
            dash_pattern,
        };
    }

    if let Some(border) = dict.get(&Name::new("Border")).and_then(|o| o.as_array()) {
        let width = border.get(2).and_then(get_number).unwrap_or(1.0);
        let dash_pattern = border
            .get(3..)
            .map(|slice| slice.iter().filter_map(get_number).collect::<Vec<f64>>())
            .unwrap_or_default();
        return AnnotationBorderStyle {
            width,
            style: if dash_pattern.is_empty() {
                BorderStyle::Solid
            } else {
                BorderStyle::Dashed
            },
            dash_pattern,
        };
    }

    AnnotationBorderStyle::default()
}

/// Parse /AP (Appearance) entry.
///
/// Per PDF spec, /N can be either a direct stream or a dictionary of appearance
/// states (e.g. form widgets with "Yes"/"Off" states). When /N is a dict, we use
/// the first stream value found (e.g. "Off", "Yes") or None if none found.
pub fn parse_appearance(dict: &Dict) -> Option<AnnotationAppearance> {
    let ap = dict.get(&Name::new("AP")).and_then(|o| o.as_dict())?;
    let mut result = AnnotationAppearance {
        bbox: [0.0, 0.0, 0.0, 0.0],
        ..Default::default()
    };

    fn extract_stream_and_bbox(obj: &Object) -> Option<(Vec<u8>, [f64; 4])> {
        match obj {
            Object::Stream { dict: sd, data } => {
                let mut bbox = [0.0, 0.0, 0.0, 0.0];
                if let Some(bbox_arr) = sd.get(&Name::new("BBox")).and_then(|o| o.as_array()) {
                    if bbox_arr.len() >= 4 {
                        bbox = [
                            bbox_arr.get(0).and_then(get_number).unwrap_or(0.0),
                            bbox_arr.get(1).and_then(get_number).unwrap_or(0.0),
                            bbox_arr.get(2).and_then(get_number).unwrap_or(0.0),
                            bbox_arr.get(3).and_then(get_number).unwrap_or(0.0),
                        ];
                    }
                }
                Some((data.clone(), bbox))
            }
            Object::Dict(state_dict) => {
                // Try common form widget state names first, then first stream found
                for key in ["Off", "Yes", "1", "0"] {
                    if let Some(stream_obj) = state_dict.get(&Name::new(key)) {
                        if let Some((data, bbox)) = extract_stream_and_bbox(stream_obj) {
                            return Some((data, bbox));
                        }
                    }
                }
                // Sort keys for deterministic iteration (HashMap order is unspecified)
                let mut keys: Vec<_> = state_dict.keys().collect();
                keys.sort_by(|a, b| a.as_str().cmp(b.as_str()));
                for k in keys {
                    if let Some((data, bbox)) = extract_stream_and_bbox(state_dict.get(k).unwrap())
                    {
                        return Some((data, bbox));
                    }
                }
                None
            }
            _ => None,
        }
    }

    if let Some(n_obj) = ap.get(&Name::new("N")) {
        if let Some((data, bbox)) = extract_stream_and_bbox(n_obj) {
            result.normal = Some(data);
            result.bbox = bbox;
        }
    }
    if let Some(Object::Stream { data, .. }) = ap.get(&Name::new("R")) {
        result.rollover = Some(data.clone());
    }
    if let Some(Object::Stream { data, .. }) = ap.get(&Name::new("D")) {
        result.down = Some(data.clone());
    }

    if result.normal.is_some() || result.rollover.is_some() || result.down.is_some() {
        Some(result)
    } else {
        None
    }
}

// ============================================================================
// Content Stream Generation
// ============================================================================

/// Resources required by a generated appearance stream (e.g. fonts).
/// Callers must ensure these are present in the page's resource dictionary
/// (e.g. `/Resources << /Font << /Helvetica ... >> >>`) when embedding the stream.
#[derive(Debug, Clone, Default)]
pub struct AppearanceResources {
    /// Font names required by the content stream (e.g. `["Helvetica"]`).
    pub fonts: Vec<&'static str>,
}

/// Result of appearance generation: content stream bytes and required resources.
pub type AppearanceResult = (Vec<u8>, AppearanceResources);

/// Sanitize an f64 for use in geometry; use default if NaN or infinite.
fn sanitize_f64(v: f64, default: f64) -> f64 {
    if v.is_finite() { v } else { default }
}

/// Extract and sanitize rect coordinates; use 0.0 for any non-finite component.
fn sanitize_rect(rect: &[f64; 4]) -> [f64; 4] {
    [
        sanitize_f64(rect.get(0).copied().unwrap_or(0.0), 0.0),
        sanitize_f64(rect.get(1).copied().unwrap_or(0.0), 0.0),
        sanitize_f64(rect.get(2).copied().unwrap_or(0.0), 0.0),
        sanitize_f64(rect.get(3).copied().unwrap_or(0.0), 0.0),
    ]
}

/// Escape text for use in PDF content stream string literals.
/// Escapes: backslash, parens, and special chars (\r \n \f \t).
/// Uses single-pass character-by-character escaping to avoid double-escaping
/// (chained .replace() would incorrectly re-escape already-escaped sequences).
///
/// TODO: Text in content streams is limited to ASCII/Latin-1. Proper Unicode
/// support requires UTF-16BE encoding with BOM (e.g. `\376\377` + UTF-16BE
/// bytes) per PDF spec for text in annotations.
fn escape_text_for_content_stream(text: &str) -> String {
    let mut result = String::with_capacity(text.len());
    for ch in text.chars() {
        match ch {
            '\\' => result.push_str("\\\\"),
            '(' => result.push_str("\\("),
            ')' => result.push_str("\\)"),
            '\r' => result.push_str("\\r"),
            '\n' => result.push_str("\\n"),
            '\x0c' => result.push_str("\\f"),
            '\t' => result.push_str("\\t"),
            _ => result.push(ch),
        }
    }
    result
}

/// Generate content stream for caret annotation (upward-pointing triangle).
///
/// # Limitation
/// Currently only supports the default upward-pointing caret. Extended caret
/// symbols (paragraph, new paragraph, section) are not yet implemented.
/// TODO: Add extended caret symbol handling per PDF spec.
pub fn generate_caret_appearance(rect: &[f64; 4], color: &[f64]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);
    let cx = (x0 + x1) / 2.0;
    let w = (x1 - x0) * 0.4;

    let r = sanitize_f64(color.get(0).copied().unwrap_or(0.0), 0.0);
    let g = sanitize_f64(color.get(1).copied().unwrap_or(0.0), 0.0);
    let b = sanitize_f64(color.get(2).copied().unwrap_or(0.0), 0.0);

    let stream = format!(
        "q {} {} {} rg {} {} m {} {} l {} {} l h f Q\n",
        r,
        g,
        b,
        cx,
        y0,
        cx - w,
        y1,
        cx + w,
        y1,
    )
    .into_bytes();
    (stream, AppearanceResources::default())
}

/// Generate content stream for file attachment icon (paperclip or pushpin)
pub fn generate_file_attachment_icon(icon: &AnnotationIcon, rect: &[f64; 4]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);
    let cx = (x0 + x1) / 2.0;
    let cy = (y0 + y1) / 2.0;
    let w = ((x1 - x0).min(y1 - y0)) * 0.3;

    let path = match icon {
        AnnotationIcon::GraphPushPin => {
            format!(
                "q 0.3 0.3 0.3 rg {} {} m {} {} l {} {} l h f Q\n",
                cx - w,
                cy + w,
                cx,
                cy - w,
                cx + w,
                cy + w,
            )
        }
        _ => {
            // PaperclipTag - simplified paperclip shape
            format!(
                "q 0.2 0.2 0.2 rg {} {} m {} {} l {} {} l {} {} l h f Q\n",
                cx - w,
                cy,
                cx,
                cy + w,
                cx + w,
                cy,
                cx,
                cy - w,
            )
        }
    };
    (path.into_bytes(), AppearanceResources::default())
}

/// Generate content stream for sound icon (speaker)
/// Uses Bézier curve approximation (4 curves) since PDF has no `arc` operator.
pub fn generate_sound_icon(rect: &[f64; 4]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);
    let cx = (x0 + x1) / 2.0;
    let cy = (y0 + y1) / 2.0;
    let r = ((x1 - x0).min(y1 - y0)) * 0.35;
    let k = BEZIER_CIRCLE_MAGIC;

    let stream = format!(
        "q 0.2 0.2 0.2 rg {} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c h f Q\n",
        cx + r, cy,
        cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r,
        cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy,
        cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r,
        cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy,
    )
    .into_bytes();
    (stream, AppearanceResources::default())
}

/// Generate content stream for stamp text in rounded rectangle.
///
/// # Font resources
/// The caller must ensure `/Helvetica` is available in the page's resource
/// dictionary (e.g. via `/Font << /Helvetica ... >>`). See `AppearanceResources`.
pub fn generate_stamp_appearance(text: &str, rect: &[f64; 4], color: &[f64]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);
    let r = sanitize_f64(color.get(0).copied().unwrap_or(1.0), 1.0);
    let g = sanitize_f64(color.get(1).copied().unwrap_or(0.0), 0.0);
    let b = sanitize_f64(color.get(2).copied().unwrap_or(0.0), 0.0);

    let escaped = escape_text_for_content_stream(text);
    let stream = format!(
        "q {} {} {} rg\n\
         {} {} m {} {} l {} {} l {} {} l h\n\
         f\n\
         BT /Helvetica 12 Tf {} {} Td ({}) Tj ET\n\
         Q\n",
        r,
        g,
        b,
        x0,
        y0,
        x1,
        y0,
        x1,
        y1,
        x0,
        y1,
        x0 + 5.0,
        (y0 + y1) / 2.0 - 6.0,
        escaped,
    )
    .into_bytes();
    (
        stream,
        AppearanceResources {
            fonts: vec!["Helvetica"],
        },
    )
}

/// Generate content stream for redaction overlay (black rectangle with red X)
pub fn generate_redact_appearance(rect: &[f64; 4]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);

    let stream = format!(
        "q 0 0 0 rg {} {} {} {} re f 1 0 0 RG 2 w {} {} m {} {} l {} {} m {} {} l S Q\n",
        x0,
        y0,
        x1 - x0,
        y1 - y0,
        x0,
        y0,
        x1,
        y1,
        x0,
        y1,
        x1,
        y0,
    )
    .into_bytes();
    (stream, AppearanceResources::default())
}

/// Generate content stream for watermark (diagonal text).
/// Uses `cm` (concat matrix) for rotation since PDF has no `rotate` operator.
///
/// # Font resources
/// The caller must ensure `/Helvetica` is available in the page's resource
/// dictionary (e.g. via `/Font << /Helvetica ... >>`). See `AppearanceResources`.
///
/// TODO: Opacity via ExtGState is not yet implemented.
pub fn generate_watermark_appearance(text: &str, rect: &[f64; 4]) -> AppearanceResult {
    let [x0, y0, x1, y1] = sanitize_rect(rect);
    let cx = (x0 + x1) / 2.0;
    let cy = (y0 + y1) / 2.0;

    let escaped = escape_text_for_content_stream(text);

    // 45 degrees: cos(45°)=sin(45°)≈0.7071. Rotation matrix: cos sin -sin cos 0 0
    let cos_45 = std::f64::consts::FRAC_1_SQRT_2;
    let sin_45 = std::f64::consts::FRAC_1_SQRT_2;

    let stream = format!(
        "q 0.7 0.7 0.7 rg {} {} {} {} 0 0 cm BT /Helvetica 24 Tf {} {} Td ({}) Tj ET Q\n",
        cos_45,
        sin_45,
        -sin_45,
        cos_45,
        cx - 50.0,
        cy - 6.0,
        escaped,
    )
    .into_bytes();
    (
        stream,
        AppearanceResources {
            fonts: vec!["Helvetica"],
        },
    )
}

/// Check if annotation should be rendered based on flags (not hidden, not invisible)
pub fn should_render(flags: u32) -> bool {
    use crate::pdf::annot_types::AnnotationFlags;
    (flags & AnnotationFlags::HIDDEN) == 0 && (flags & AnnotationFlags::INVISIBLE) == 0
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_annotation_type_from_name_all_28() {
        let names = [
            "Text",
            "Link",
            "FreeText",
            "Line",
            "Square",
            "Circle",
            "Polygon",
            "PolyLine",
            "Highlight",
            "Underline",
            "Squiggly",
            "StrikeOut",
            "Stamp",
            "Ink",
            "Caret",
            "Popup",
            "FileAttachment",
            "Sound",
            "Movie",
            "Screen",
            "Widget",
            "PrinterMark",
            "TrapNet",
            "Watermark",
            "3D",
            "Redact",
            "Projection",
            "RichMedia",
        ];
        for name in names {
            assert!(
                AnnotationType::from_name(name).is_some(),
                "Type {} should parse",
                name
            );
        }
    }

    #[test]
    fn test_annotation_type_unknown_returns_none() {
        assert!(AnnotationType::from_name("Unknown").is_none());
        assert!(AnnotationType::from_name("").is_none());
        assert!(AnnotationType::from_name("InvalidType").is_none());
    }

    #[test]
    fn test_annotation_type_name_roundtrip() {
        let types = [
            AnnotationType::Text,
            AnnotationType::Link,
            AnnotationType::FreeText,
            AnnotationType::Stamp,
            AnnotationType::FileAttachment,
            AnnotationType::Sound,
            AnnotationType::ThreeD,
            AnnotationType::Redact,
        ];
        for t in types {
            assert_eq!(
                AnnotationType::from_name(t.name()),
                Some(t),
                "Roundtrip for {:?}",
                t
            );
        }
    }

    #[test]
    fn test_markup_classification() {
        assert!(AnnotationType::Text.is_markup());
        assert!(AnnotationType::Highlight.is_markup());
        assert!(AnnotationType::Stamp.is_markup());
        assert!(!AnnotationType::Link.is_markup());
        assert!(!AnnotationType::Popup.is_markup());
        // Widget, PrinterMark, TrapNet are NOT markup per PDF spec
        assert!(!AnnotationType::Widget.is_markup());
        assert!(!AnnotationType::PrinterMark.is_markup());
        assert!(!AnnotationType::TrapNet.is_markup());
    }

    #[test]
    fn test_default_icons() {
        assert_eq!(
            AnnotationType::Text.default_icon(),
            Some(AnnotationIcon::Note)
        );
        assert_eq!(
            AnnotationType::FileAttachment.default_icon(),
            Some(AnnotationIcon::PaperclipTag)
        );
        assert_eq!(
            AnnotationType::Sound.default_icon(),
            Some(AnnotationIcon::Speaker)
        );
        assert!(AnnotationType::Link.default_icon().is_none());
        assert!(AnnotationType::Stamp.default_icon().is_none());
    }

    #[test]
    fn test_parse_annotation_from_dict() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        dict.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Real(10.0),
                Object::Real(20.0),
                Object::Real(100.0),
                Object::Real(80.0),
            ]),
        );
        dict.insert(Name::new("F"), Object::Int(4)); // PRINT flag

        let (t, rect, flags) = parse_annotation(&dict).expect("valid annotation");
        assert_eq!(t, AnnotationType::Text);
        assert_eq!(rect, [10.0, 20.0, 100.0, 80.0]);
        assert_eq!(flags, 4);
    }

    #[test]
    fn test_parse_annotation_missing_subtype() {
        let dict: Dict = HashMap::new();
        assert!(parse_annotation(&dict).is_err());
    }

    #[test]
    fn test_parse_annotation_unknown_subtype() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("UnknownType")));
        assert!(parse_annotation(&dict).is_err());
    }

    #[test]
    fn test_parse_border_style_from_bs() {
        let mut dict: Dict = HashMap::new();
        let mut bs: Dict = HashMap::new();
        bs.insert(Name::new("W"), Object::Real(2.0));
        bs.insert(Name::new("S"), Object::Name(Name::new("D")));
        bs.insert(
            Name::new("D"),
            Object::Array(vec![Object::Real(3.0), Object::Real(2.0)]),
        );
        dict.insert(Name::new("BS"), Object::Dict(bs));

        let border = parse_border_style(&dict);
        assert_eq!(border.width, 2.0);
        assert_eq!(border.style, BorderStyle::Dashed);
        assert_eq!(border.dash_pattern, vec![3.0, 2.0]);
    }

    #[test]
    fn test_parse_border_style_from_border() {
        let mut dict: Dict = HashMap::new();
        dict.insert(
            Name::new("Border"),
            Object::Array(vec![
                Object::Int(0),
                Object::Int(0),
                Object::Real(1.5),
                Object::Real(3.0),
                Object::Real(2.0),
            ]),
        );

        let border = parse_border_style(&dict);
        assert_eq!(border.width, 1.5);
        assert_eq!(border.dash_pattern, vec![3.0, 2.0]);
    }

    #[test]
    fn test_parse_border_style_default() {
        let dict: Dict = HashMap::new();
        let border = parse_border_style(&dict);
        assert_eq!(border.width, 1.0);
        assert_eq!(border.style, BorderStyle::Solid);
        assert!(border.dash_pattern.is_empty());
    }

    #[test]
    fn test_should_render_flags() {
        assert!(should_render(AnnotationFlags::PRINT));
        assert!(!should_render(AnnotationFlags::HIDDEN));
        assert!(!should_render(AnnotationFlags::INVISIBLE));
        assert!(!should_render(
            AnnotationFlags::HIDDEN | AnnotationFlags::PRINT
        ));
    }

    #[test]
    fn test_generate_caret_appearance() {
        let rect = [0.0, 0.0, 20.0, 20.0];
        let color = [1.0, 0.0, 0.0];
        let (stream, resources) = generate_caret_appearance(&rect, &color);
        assert!(!stream.is_empty());
        assert!(resources.fonts.is_empty());
        let s = String::from_utf8(stream).expect("valid UTF-8");
        assert!(s.contains("rg"));
        assert!(s.contains("m"));
        assert!(s.contains("l"));
        assert!(s.contains("f"));
    }

    #[test]
    fn test_generate_file_attachment_icon() {
        let rect = [0.0, 0.0, 24.0, 24.0];
        let (stream, _) = generate_file_attachment_icon(&AnnotationIcon::PaperclipTag, &rect);
        assert!(!stream.is_empty());
    }

    #[test]
    fn test_generate_sound_icon() {
        let rect = [0.0, 0.0, 24.0, 24.0];
        let (stream, _) = generate_sound_icon(&rect);
        assert!(!stream.is_empty());
        let s = String::from_utf8(stream).expect("valid UTF-8");
        assert!(s.contains(" c "), "should use Bézier curveto, not arc");
    }

    #[test]
    fn test_generate_stamp_appearance() {
        let rect = [0.0, 0.0, 100.0, 40.0];
        let color = [1.0, 0.0, 0.0];
        let (stream, resources) = generate_stamp_appearance("DRAFT", &rect, &color);
        assert!(!stream.is_empty());
        assert_eq!(resources.fonts, ["Helvetica"]);
        let s = String::from_utf8(stream).expect("valid UTF-8");
        assert!(s.contains("DRAFT"));
    }

    #[test]
    fn test_generate_redact_appearance() {
        let rect = [0.0, 0.0, 100.0, 50.0];
        let (stream, _) = generate_redact_appearance(&rect);
        assert!(!stream.is_empty());
        let s = String::from_utf8(stream).expect("valid UTF-8");
        assert!(s.contains("re"));
        assert!(s.contains("f"));
    }

    #[test]
    fn test_generate_watermark_appearance() {
        let rect = [0.0, 0.0, 200.0, 100.0];
        let (stream, resources) = generate_watermark_appearance("CONFIDENTIAL", &rect);
        assert!(!stream.is_empty());
        assert_eq!(resources.fonts, ["Helvetica"]);
        let s = String::from_utf8(stream).expect("valid UTF-8");
        assert!(s.contains("CONFIDENTIAL"));
    }

    #[test]
    fn test_parse_appearance_empty_dict() {
        let dict: Dict = HashMap::new();
        assert!(parse_appearance(&dict).is_none());
    }

    #[test]
    fn test_annotation_appearance_default() {
        let app = AnnotationAppearance::default();
        assert!(app.normal.is_none());
        assert!(app.rollover.is_none());
        assert!(app.down.is_none());
        assert_eq!(app.bbox, [0.0, 0.0, 0.0, 0.0]);
    }

    #[test]
    fn test_annotation_icon_names() {
        assert_eq!(AnnotationIcon::Note.name(), "Note");
        assert_eq!(AnnotationIcon::Speaker.name(), "Speaker");
        assert_eq!(AnnotationIcon::PaperclipTag.name(), "PaperclipTag");
    }

    #[test]
    fn test_parse_annotation_missing_rect_returns_error() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        assert!(
            parse_annotation(&dict).is_err(),
            "Rect is required per PDF spec"
        );
    }

    #[test]
    fn test_parse_annotation_normalizes_rect() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        // Rect with llx > urx and lly > ury (reversed coordinates)
        dict.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Real(100.0),
                Object::Real(80.0),
                Object::Real(10.0),
                Object::Real(20.0),
            ]),
        );
        let (_, rect, _) = parse_annotation(&dict).expect("valid annotation");
        assert_eq!(rect, [10.0, 20.0, 100.0, 80.0], "rect should be normalized");
    }

    #[test]
    fn test_parse_rect_accepts_int_values() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        // Rect with integer coordinates (common in PDFs)
        dict.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Int(10),
                Object::Int(20),
                Object::Int(100),
                Object::Int(80),
            ]),
        );
        let (_, rect, _) = parse_annotation(&dict).expect("valid annotation");
        assert_eq!(rect, [10.0, 20.0, 100.0, 80.0]);
    }

    #[test]
    fn test_parse_border_style_accepts_int_width() {
        let mut dict: Dict = HashMap::new();
        let mut bs: Dict = HashMap::new();
        bs.insert(Name::new("W"), Object::Int(2));
        bs.insert(Name::new("S"), Object::Name(Name::new("D")));
        bs.insert(
            Name::new("D"),
            Object::Array(vec![Object::Int(3), Object::Int(2)]),
        );
        dict.insert(Name::new("BS"), Object::Dict(bs));

        let border = parse_border_style(&dict);
        assert_eq!(border.width, 2.0);
        assert_eq!(border.dash_pattern, vec![3.0, 2.0]);
    }

    #[test]
    fn test_parse_rect_rejects_non_finite() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        dict.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Real(10.0),
                Object::Real(20.0),
                Object::Real(f64::NAN),
                Object::Real(80.0),
            ]),
        );
        assert!(
            parse_annotation(&dict).is_err(),
            "Rect with NaN should be rejected"
        );

        let mut dict_inf: Dict = HashMap::new();
        dict_inf.insert(Name::new("Subtype"), Object::Name(Name::new("Text")));
        dict_inf.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Real(10.0),
                Object::Real(f64::INFINITY),
                Object::Real(100.0),
                Object::Real(80.0),
            ]),
        );
        assert!(
            parse_annotation(&dict_inf).is_err(),
            "Rect with Infinity should be rejected"
        );
    }

    #[test]
    fn test_escape_text_for_content_stream_no_double_escape() {
        // Single-pass escaping: input "test\(paren)" -> "test\\\(paren\)"
        // (backslash and parens escaped once; chained .replace() would double-escape)
        assert_eq!(
            escape_text_for_content_stream(r"test\(paren)"),
            "test\\\\\\(paren\\)"
        );
    }

    #[test]
    fn test_parse_annotation_default_flags() {
        let mut dict: Dict = HashMap::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Link")));
        dict.insert(
            Name::new("Rect"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(10.0),
                Object::Real(10.0),
            ]),
        );
        let (_, _, flags) = parse_annotation(&dict).expect("valid annotation");
        assert_eq!(flags, 0, "default flags should be 0 per PDF spec");
    }
}
