//! PDF write module - serializes PDF objects and documents to byte streams.
//!
//! ## Sparse object number limitation
//!
//! The writer requires object numbers to be reasonably dense. If the gap between
//! consecutive used object numbers exceeds [`MAX_OBJECT_GAP`], writing fails with
//! an error. This prevents generating huge xref tables (e.g., objects 1 and 1000000
//! would otherwise create ~999998 free entries). Use sequential object numbers
//! (1, 2, 3, ...) or keep gaps under the limit.

/// Maximum allowed gap between consecutive used object numbers.
/// Larger gaps cause excessive free entries in the xref table.
pub const MAX_OBJECT_GAP: i32 = 1000;

use crate::fitz::error::{Error, Result};
use crate::pdf::object::{Array, Dict, Name, ObjRef, Object, PdfString};
use flate2::Compression;
use flate2::write::DeflateEncoder;
use std::borrow::Cow;
use std::collections::HashSet;
use std::io::Write;

/// Convert usize to i64 for PDF offsets/lengths; errors if out of range.
fn usize_to_i64(n: usize) -> Result<i64> {
    i64::try_from(n).map_err(|_| Error::limit("Value exceeds PDF maximum (i64::MAX)"))
}

/// Options for PDF writing (reserved for future use).
/// TODO: integrate with PdfWriter when incremental/linearize/ascii_hex are implemented.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct WriteOptions {
    /// Apply FlateDecode to streams (default true)
    pub compress_streams: bool,
    /// Use ASCIIHex for binary data (default false)
    pub ascii_hex: bool,
    /// Write incremental update (default false)
    pub incremental: bool,
    /// Linearize output (default false)
    pub linearize: bool,
}

impl Default for WriteOptions {
    fn default() -> Self {
        Self {
            compress_streams: true,
            ascii_hex: false,
            incremental: false,
            linearize: false,
        }
    }
}

/// PDF document writer - serializes objects and documents to byte streams
#[derive(Debug, Clone)]
pub struct PdfWriter {
    /// Stored objects: (obj_num, generation, object)
    objects: Vec<(i32, u16, Object)>,
    /// Object numbers already added (for duplicate detection)
    objects_seen: HashSet<i32>,
    /// PDF version (major, minor), default (1, 7)
    version: (u8, u8),
    /// Trailer dictionary entries
    trailer_dict: Dict,
    /// Whether to compress streams with FlateDecode
    compress: bool,
}

impl PdfWriter {
    /// Create new writer with default settings
    pub fn new() -> Self {
        Self {
            objects: Vec::new(),
            objects_seen: HashSet::new(),
            version: (1, 7),
            trailer_dict: Dict::new(),
            compress: true,
        }
    }

    /// Create with specific PDF version
    pub fn with_version(major: u8, minor: u8) -> Self {
        Self {
            objects: Vec::new(),
            objects_seen: HashSet::new(),
            version: (major, minor),
            trailer_dict: Dict::new(),
            compress: true,
        }
    }

    /// Add an object to write
    pub fn add_object(&mut self, obj_num: i32, generation: u16, object: Object) -> Result<()> {
        if obj_num == 0 {
            return Err(Error::generic(
                "Object number 0 is reserved and cannot be used for user objects",
            ));
        }
        if self.objects_seen.contains(&obj_num) {
            return Err(Error::generic(format!(
                "Duplicate object number: {}",
                obj_num
            )));
        }
        self.objects_seen.insert(obj_num);
        self.objects.push((obj_num, generation, object));
        Ok(())
    }

    /// Set a trailer dictionary entry
    pub fn set_trailer(&mut self, key: Name, value: Object) {
        self.trailer_dict.insert(key, value);
    }

    /// Enable or disable stream compression
    pub fn set_compress(&mut self, compress: bool) {
        self.compress = compress;
    }

    /// PDF version (major, minor)
    pub fn version(&self) -> (u8, u8) {
        self.version
    }

    /// Stored objects (obj_num, generation, object)
    pub fn objects(&self) -> &[(i32, u16, Object)] {
        &self.objects
    }

    /// Whether stream compression is enabled
    pub fn compress(&self) -> bool {
        self.compress
    }

    /// Whether the trailer contains the given key
    pub fn trailer_contains_key(&self, key: &Name) -> bool {
        self.trailer_dict.contains_key(key)
    }

    /// Write complete PDF to the given writer
    pub fn write(&self, w: &mut dyn std::io::Write) -> Result<()> {
        let mut buf = Vec::new();

        // Header: %PDF-M.m\n%âãÏÓ\n
        buf.write_all(format!("%PDF-{}.{}\n", self.version.0, self.version.1).as_bytes())
            .map_err(Error::System)?;
        // PDF binary comment %âãÏÓ (§7.5.2): high-bit bytes prevent text processors from treating the file as ASCII
        buf.write_all(&[0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A])
            .map_err(Error::System)?;

        // Sort objects by obj_num for deterministic output
        let mut sorted: Vec<_> = self.objects.iter().collect();
        sorted.sort_by_key(|(num, _, _)| *num);

        // Track in-use offsets: obj_num -> (generation, offset)
        let mut in_use: std::collections::BTreeMap<i32, (u16, i64)> =
            std::collections::BTreeMap::new();

        for (obj_num, generation, obj) in &sorted {
            let offset = usize_to_i64(buf.len())?;
            let bytes = write_indirect_object(&mut buf, *obj_num, *generation, obj, self.compress)?;
            if bytes == 0 {
                return Err(Error::generic(
                    "Indirect object must write at least one byte",
                ));
            }
            in_use.insert(*obj_num, (*generation, offset));
        }

        // Size = max_obj_num + 1; xref must have ALL objects 0..Size-1 (PDF spec)
        let max_obj_num = in_use.keys().copied().max().unwrap_or(0);
        let size = max_obj_num
            .checked_add(1)
            .ok_or_else(|| Error::limit("Object count exceeds PDF maximum"))?;

        // Enforce sparse object limit: gaps between consecutive used objects must not exceed MAX_OBJECT_GAP.
        // Otherwise we'd generate huge free lists (e.g., objects 1 and 1000000 → ~999998 free entries).
        let mut used_nums: Vec<i32> = in_use.keys().copied().collect();
        used_nums.sort();
        for w in used_nums.windows(2) {
            let gap = w[1] - w[0] - 1;
            if gap > MAX_OBJECT_GAP {
                return Err(Error::limit(format!(
                    "Sparse object numbers not supported: gap of {} between objects {} and {} exceeds maximum of {}. Use sequential object numbers or keep gaps under {}.",
                    gap, w[0], w[1], MAX_OBJECT_GAP, MAX_OBJECT_GAP
                )));
            }
        }

        // Build free list: object numbers that are free (0 + gaps)
        let free_list: Vec<i32> = (0..size).filter(|n| !in_use.contains_key(n)).collect();
        // Build next-free map: each free entry's offset = next free obj num (or 0 for last)
        let next_free: std::collections::BTreeMap<i32, i64> = free_list
            .iter()
            .enumerate()
            .map(|(i, &obj)| {
                let next = if i + 1 < free_list.len() {
                    i64::from(free_list[i + 1])
                } else {
                    0
                };
                (obj, next)
            })
            .collect();

        // Build complete offsets for ALL 0..size
        let mut offsets: Vec<(i32, u16, i64)> = Vec::with_capacity(size as usize);
        for n in 0..size {
            if let Some(&(generation, off)) = in_use.get(&n) {
                offsets.push((n, generation, off));
            } else {
                let next = next_free.get(&n).copied().unwrap_or(0);
                offsets.push((n, 65535, next));
            }
        }

        // Write xref table
        let xref_offset = usize_to_i64(buf.len())?;
        write_xref_table(&mut buf, &offsets)?;

        // Trailer - Size must be one greater than the highest object number (PDF spec).
        let mut trailer = self.trailer_dict.clone();
        trailer.insert(Name::new("Size"), Object::Int(i64::from(size)));

        buf.write_all(b"trailer\n").map_err(Error::System)?;
        write_object(&mut buf, &Object::Dict(trailer))?;
        buf.write_all(b"\nstartxref\n").map_err(Error::System)?;
        buf.write_all(format!("{}\n", xref_offset).as_bytes())
            .map_err(Error::System)?;
        buf.write_all(b"%%EOF\n").map_err(Error::System)?;

        w.write_all(&buf).map_err(Error::System)?;
        Ok(())
    }

    /// Convenience: write to Vec<u8>
    pub fn write_to_vec(&self) -> Result<Vec<u8>> {
        let mut out = Vec::new();
        self.write(&mut out)?;
        Ok(out)
    }
}

impl Default for PdfWriter {
    fn default() -> Self {
        Self::new()
    }
}

/// Escape bytes for PDF string literal - parens, backslash, non-printable
pub fn escape_pdf_string(s: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(s.len() * 2);
    for &b in s {
        match b {
            b'\\' => out.extend_from_slice(b"\\\\"),
            b'(' => out.extend_from_slice(b"\\("),
            b')' => out.extend_from_slice(b"\\)"),
            b'\r' => out.extend_from_slice(b"\\r"),
            b'\n' => out.extend_from_slice(b"\\n"),
            b'\t' => out.extend_from_slice(b"\\t"),
            b'\x08' => out.extend_from_slice(b"\\b"),
            b'\x0c' => out.extend_from_slice(b"\\f"),
            0x00..=0x1f | 0x7f..=0xff => {
                out.extend_from_slice(format!("\\{:03o}", b).as_bytes());
            }
            _ => out.push(b),
        }
    }
    out
}

/// Escape name for PDF - # encoding for special chars.
/// Escapes: all control chars (0x00-0x20), non-ASCII (>0x7E), and PDF delimiter chars
/// (# / ( ) < > [ ] { } %).
pub fn escape_pdf_name(name: &str) -> String {
    let mut out = String::with_capacity(name.len() * 3);
    for b in name.bytes() {
        let needs_escape = b <= 0x20
            || b > 0x7E
            || b == b'#'
            || b == b'/'
            || b == b'('
            || b == b')'
            || b == b'<'
            || b == b'>'
            || b == b'['
            || b == b']'
            || b == b'{'
            || b == b'}'
            || b == b'%';
        if needs_escape {
            out.push_str(&format!("#{:02X}", b));
        } else {
            out.push(b as char);
        }
    }
    out
}

/// Format float: strip trailing zeros, max 6 decimal places, use integers when possible.
/// Returns error for NaN and Infinity (invalid in PDF).
pub fn format_real(f: f64) -> Result<String> {
    if !f.is_finite() {
        return Err(Error::generic(
            "PDF does not support NaN or Infinity in numeric values",
        ));
    }
    Ok(if f.fract() == 0.0 && f.abs() < 1e15 {
        let truncated = f.trunc();
        if truncated >= i64::MIN as f64 && truncated <= i64::MAX as f64 {
            format!("{}", truncated as i64)
        } else {
            let s = format!("{:.6}", f);
            s.trim_end_matches('0').trim_end_matches('.').to_string()
        }
    } else {
        let s = format!("{:.6}", f);
        let s = s.trim_end_matches('0').trim_end_matches('.');
        s.to_string()
    })
}

/// Write a single PDF object value
pub fn write_object(w: &mut dyn std::io::Write, obj: &Object) -> Result<()> {
    match obj {
        Object::Null => w.write_all(b"null").map_err(Error::System),
        Object::Bool(true) => w.write_all(b"true").map_err(Error::System),
        Object::Bool(false) => w.write_all(b"false").map_err(Error::System),
        Object::Int(i) => w
            .write_all(format!("{}", i).as_bytes())
            .map_err(Error::System),
        Object::Real(f) => w
            .write_all(format_real(*f)?.as_bytes())
            .map_err(Error::System),
        Object::String(s) => {
            w.write_all(b"(").map_err(Error::System)?;
            let escaped = escape_pdf_string(s.as_bytes());
            w.write_all(&escaped).map_err(Error::System)?;
            w.write_all(b")").map_err(Error::System)
        }
        Object::Name(n) => {
            w.write_all(b"/").map_err(Error::System)?;
            let escaped = escape_pdf_name(n.as_str());
            w.write_all(escaped.as_bytes()).map_err(Error::System)
        }
        Object::Array(a) => {
            w.write_all(b"[").map_err(Error::System)?;
            for (i, elem) in a.iter().enumerate() {
                if i > 0 {
                    w.write_all(b" ").map_err(Error::System)?;
                }
                write_object(w, elem)?;
            }
            w.write_all(b"]").map_err(Error::System)
        }
        Object::Dict(d) => {
            w.write_all(b"<<").map_err(Error::System)?;
            let mut sorted: Vec<_> = d.iter().collect();
            sorted.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
            for (k, v) in sorted {
                w.write_all(b" ").map_err(Error::System)?;
                write_object(w, &Object::Name(k.clone()))?;
                w.write_all(b" ").map_err(Error::System)?;
                write_object(w, v)?;
            }
            w.write_all(b" >>").map_err(Error::System)
        }
        Object::Stream { dict, data } => write_stream_body(w, dict, data),
        Object::Ref(r) => w
            .write_all(format!("{} {} R", r.num, r.generation).as_bytes())
            .map_err(Error::System),
    }
}

/// Write stream body: dict with Length + stream data. Shared by write_object and write_indirect_object.
fn write_stream_body(w: &mut dyn std::io::Write, dict: &Dict, data: &[u8]) -> Result<()> {
    let mut d = dict.clone();
    if !d.contains_key(&Name::new("Length")) {
        d.insert(Name::new("Length"), Object::Int(usize_to_i64(data.len())?));
    }
    write_object(w, &Object::Dict(d))?;
    w.write_all(b"\nstream\n").map_err(Error::System)?;
    w.write_all(data).map_err(Error::System)?;
    w.write_all(b"\nendstream").map_err(Error::System)
}

/// Write indirect object: N G obj\n...object...\nendobj\n, return bytes written
pub fn write_indirect_object(
    w: &mut dyn std::io::Write,
    num: i32,
    generation: u16,
    obj: &Object,
    compress: bool,
) -> Result<usize> {
    let mut buf = Vec::new();
    let mut cursor = std::io::Cursor::new(&mut buf);

    cursor
        .write_all(format!("{} {} obj\n", num, generation).as_bytes())
        .map_err(Error::System)?;

    match obj {
        Object::Stream { dict, data } => {
            let (write_dict, write_data): (Dict, Cow<[u8]>) = if compress
                && !dict.contains_key(&Name::new("Filter"))
                && data.len() >= 100
            {
                let compressed = {
                    let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
                    enc.write_all(data).map_err(Error::System)?;
                    enc.finish().map_err(Error::System)?
                };
                // Only use compression if at least 10% smaller; otherwise use raw to avoid overhead
                if (compressed.len() as f64) < (data.len() as f64) * 0.9 {
                    let mut new_dict = dict.clone();
                    new_dict.insert(Name::new("Filter"), Object::Name(Name::new("FlateDecode")));
                    new_dict.insert(
                        Name::new("Length"),
                        Object::Int(usize_to_i64(compressed.len())?),
                    );
                    (new_dict, Cow::Owned(compressed))
                } else {
                    let mut new_dict = dict.clone();
                    if !new_dict.contains_key(&Name::new("Length")) {
                        new_dict
                            .insert(Name::new("Length"), Object::Int(usize_to_i64(data.len())?));
                    }
                    (new_dict, Cow::Borrowed(data))
                }
            } else {
                let mut new_dict = dict.clone();
                if !new_dict.contains_key(&Name::new("Length")) {
                    new_dict.insert(Name::new("Length"), Object::Int(usize_to_i64(data.len())?));
                }
                (new_dict, Cow::Borrowed(data))
            };

            write_stream_body(&mut cursor, &write_dict, write_data.as_ref())?;
        }
        _ => write_object(&mut cursor, obj)?,
    }

    cursor.write_all(b"\nendobj\n").map_err(Error::System)?;

    let written = buf.len();
    w.write_all(&buf).map_err(Error::System)?;
    Ok(written)
}

/// Write traditional xref table.
pub fn write_xref_table(w: &mut dyn std::io::Write, offsets: &[(i32, u16, i64)]) -> Result<()> {
    w.write_all(b"xref\n").map_err(Error::System)?;

    // Group into subsections: consecutive object numbers only (split on gaps).
    // PDF spec allows free and in-use entries in the same subsection.
    let mut i = 0;
    while i < offsets.len() {
        let start_num = offsets[i].0;
        let mut count = 1;
        while i + count < offsets.len() {
            let next = &offsets[i + count];
            let count_i32 = match i32::try_from(count) {
                Ok(c) => c,
                Err(_) => break, // count overflow - start new subsection
            };
            let expected_next = match start_num.checked_add(count_i32) {
                Some(n) => n,
                None => break, // overflow - start new subsection
            };
            if next.0 == expected_next {
                count += 1;
            } else {
                break;
            }
        }
        w.write_all(format!("{} {}\n", start_num, count).as_bytes())
            .map_err(Error::System)?;
        for j in i..i + count {
            let (_obj_num, generation, offset) = offsets[j];
            let (mark, off, g) = if generation == 65535 {
                // Free entry: offset = next free object number (or 0 for last in chain)
                ("f", offset, 65535u16)
            } else {
                ("n", offset, generation)
            };
            // PDF spec: exactly 20 bytes per entry: "nnnnnnnnnn ggggg n\r\n"
            // (10-digit offset, space, 5-digit generation, space, n|f, \r\n)
            let entry = format!("{:010} {:05} {}\r\n", off, g, mark);
            if entry.len() != 20 {
                return Err(Error::generic(
                    "xref entry must be exactly 20 bytes per PDF spec",
                ));
            }
            w.write_all(entry.as_bytes()).map_err(Error::System)?;
        }
        i += count;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pdf::object::PdfString;

    fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool {
        if needle.len() > haystack.len() {
            return false;
        }
        haystack.windows(needle.len()).any(|w| w == needle)
    }

    #[test]
    fn test_write_null() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Null).unwrap();
        assert_eq!(buf, b"null");
    }

    #[test]
    fn test_write_bool_true() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Bool(true)).unwrap();
        assert_eq!(buf, b"true");
    }

    #[test]
    fn test_write_bool_false() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Bool(false)).unwrap();
        assert_eq!(buf, b"false");
    }

    #[test]
    fn test_write_int() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Int(42)).unwrap();
        assert_eq!(buf, b"42");
    }

    #[test]
    fn test_write_int_negative() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Int(-100)).unwrap();
        assert_eq!(buf, b"-100");
    }

    #[test]
    fn test_write_real() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Real(3.14)).unwrap();
        assert_eq!(buf, b"3.14");
    }

    #[test]
    fn test_format_real_integer() {
        assert_eq!(format_real(42.0).unwrap(), "42");
        assert_eq!(format_real(0.0).unwrap(), "0");
    }

    #[test]
    fn test_format_real_trailing_zeros() {
        assert_eq!(format_real(1.500).unwrap(), "1.5");
        assert_eq!(format_real(2.100).unwrap(), "2.1");
    }

    #[test]
    fn test_format_real_nan_infinity() {
        assert!(format_real(f64::NAN).is_err());
        assert!(format_real(f64::INFINITY).is_err());
        assert!(format_real(f64::NEG_INFINITY).is_err());
    }

    #[test]
    fn test_write_string() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::String(PdfString::new(b"Hello".to_vec()))).unwrap();
        assert_eq!(buf, b"(Hello)");
    }

    #[test]
    fn test_escape_pdf_string_parens() {
        let s = escape_pdf_string(b"test (parens) here");
        assert_eq!(String::from_utf8(s).unwrap(), "test \\(parens\\) here");
    }

    #[test]
    fn test_escape_pdf_string_backslash() {
        let s = escape_pdf_string(b"a\\b");
        assert_eq!(String::from_utf8(s).unwrap(), "a\\\\b");
    }

    #[test]
    fn test_escape_pdf_string_non_printable() {
        let s = escape_pdf_string(b"a\nb");
        assert_eq!(String::from_utf8(s).unwrap(), "a\\nb");
    }

    #[test]
    fn test_escape_pdf_name_simple() {
        assert_eq!(escape_pdf_name("Type"), "Type");
    }

    #[test]
    fn test_escape_pdf_name_space() {
        assert_eq!(escape_pdf_name("A B"), "A#20B");
    }

    #[test]
    fn test_escape_pdf_name_hash() {
        assert_eq!(escape_pdf_name("A#B"), "A#23B");
    }

    #[test]
    fn test_escape_pdf_name_special_chars() {
        assert_eq!(escape_pdf_name("A\x00B"), "A#00B");
        assert_eq!(escape_pdf_name("A\tB"), "A#09B");
        assert_eq!(escape_pdf_name("A\x0CB"), "A#0CB");
        assert_eq!(escape_pdf_name("A\rB"), "A#0DB");
        assert_eq!(escape_pdf_name("A\x7FB"), "A#7FB");
    }

    #[test]
    fn test_write_name() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Name(Name::new("Type"))).unwrap();
        assert_eq!(buf, b"/Type");
    }

    #[test]
    fn test_write_array() {
        let mut buf = Vec::new();
        write_object(
            &mut buf,
            &Object::Array(vec![Object::Int(1), Object::Int(2), Object::Int(3)]),
        )
        .unwrap();
        assert_eq!(buf, b"[1 2 3]");
    }

    #[test]
    fn test_write_dict() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        dict.insert(Name::new("Count"), Object::Int(1));
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Dict(dict)).unwrap();
        assert!(bytes_contains(&buf, b"/Type"));
        assert!(bytes_contains(&buf, b"/Page"));
        assert!(bytes_contains(&buf, b"/Count"));
        assert!(bytes_contains(&buf, b"1"));
    }

    #[test]
    fn test_write_ref() {
        let mut buf = Vec::new();
        write_object(&mut buf, &Object::Ref(ObjRef::new(10, 0))).unwrap();
        assert_eq!(buf, b"10 0 R");
    }

    #[test]
    fn test_write_indirect_object() {
        let mut buf = Vec::new();
        let n = write_indirect_object(&mut buf, 5, 0, &Object::Int(42), false).unwrap();
        assert!(n > 0);
        assert!(buf.starts_with(b"5 0 obj\n"));
        assert!(bytes_contains(&buf, b"42"));
        assert!(buf.ends_with(b"endobj\n"));
    }

    #[test]
    fn test_write_xref_table() {
        let mut buf = Vec::new();
        let offsets = vec![(0, 65535u16, 0i64), (1, 0u16, 100i64), (2, 0u16, 200i64)];
        write_xref_table(&mut buf, &offsets).unwrap();
        assert!(buf.starts_with(b"xref\n"));
        // PDF spec: exactly 20 bytes per entry with \r\n (no trailing space)
        assert!(bytes_contains(&buf, b"0000000000 65535 f\r\n"));
        assert!(bytes_contains(&buf, b"0000000100 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000200 00000 n\r\n"));
    }

    #[test]
    fn test_write_xref_free_object_zero() {
        let mut buf = Vec::new();
        write_xref_table(&mut buf, &[(0, 65535, 0)]).unwrap();
        assert!(bytes_contains(&buf, b"0 1\n"));
        assert!(bytes_contains(&buf, b"0000000000 65535 f\r\n"));
    }

    #[test]
    fn test_write_xref_with_gaps() {
        // Non-consecutive object numbers must produce separate subsections.
        // 0,1 are consecutive -> "0 2"; 5 and 10 are gaps -> "5 1", "10 1"
        let mut buf = Vec::new();
        let offsets = vec![
            (0, 65535u16, 0i64),
            (1, 0u16, 100i64),
            (5, 0u16, 200i64),
            (10, 0u16, 300i64),
        ];
        write_xref_table(&mut buf, &offsets).unwrap();
        assert!(buf.starts_with(b"xref\n"));
        assert!(bytes_contains(&buf, b"0 2\n")); // free + obj 1
        assert!(bytes_contains(&buf, b"5 1\n"));
        assert!(bytes_contains(&buf, b"10 1\n"));
        assert!(bytes_contains(&buf, b"0000000100 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000200 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000300 00000 n\r\n"));
    }

    #[test]
    fn test_write_xref_mixed_free_and_inuse_subsection() {
        // Objects 0,1,2,5,6 -> subsections "0 3" (free+1+2) and "5 2" (5,6)
        let mut buf = Vec::new();
        let offsets = vec![
            (0, 65535u16, 0i64),
            (1, 0u16, 100i64),
            (2, 0u16, 200i64),
            (5, 0u16, 300i64),
            (6, 0u16, 400i64),
        ];
        write_xref_table(&mut buf, &offsets).unwrap();
        assert!(buf.starts_with(b"xref\n"));
        assert!(bytes_contains(&buf, b"0 3\n"));
        assert!(bytes_contains(&buf, b"5 2\n"));
        assert!(bytes_contains(&buf, b"0000000100 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000200 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000300 00000 n\r\n"));
        assert!(bytes_contains(&buf, b"0000000400 00000 n\r\n"));
    }

    #[test]
    fn test_pdf_writer_new() {
        let w = PdfWriter::new();
        assert_eq!(w.version(), (1, 7));
        assert!(w.objects().is_empty());
        assert!(w.compress());
    }

    #[test]
    fn test_pdf_writer_with_version() {
        let w = PdfWriter::with_version(1, 4);
        assert_eq!(w.version(), (1, 4));
    }

    #[test]
    fn test_pdf_writer_add_object() {
        let mut w = PdfWriter::new();
        w.add_object(1, 0, Object::Int(42)).unwrap();
        assert_eq!(w.objects().len(), 1);
        assert_eq!(w.objects()[0].0, 1);
        assert_eq!(w.objects()[0].1, 0);
    }

    #[test]
    fn test_pdf_writer_set_trailer() {
        let mut w = PdfWriter::new();
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        assert!(w.trailer_contains_key(&Name::new("Root")));
    }

    #[test]
    fn test_pdf_writer_set_compress() {
        let mut w = PdfWriter::new();
        w.set_compress(false);
        assert!(!w.compress());
    }

    #[test]
    fn test_write_minimal_pdf() {
        let mut w = PdfWriter::new();
        w.add_object(
            1,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Catalog")));
                d.insert(Name::new("Pages"), Object::Ref(ObjRef::new(2, 0)));
                d
            }),
        )
        .unwrap();
        w.add_object(
            2,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Pages")));
                d.insert(Name::new("Count"), Object::Int(1));
                d.insert(
                    Name::new("Kids"),
                    Object::Array(vec![Object::Ref(ObjRef::new(3, 0))]),
                );
                d
            }),
        )
        .unwrap();
        w.add_object(
            3,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Page")));
                d.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
                d.insert(
                    Name::new("MediaBox"),
                    Object::Array(vec![
                        Object::Real(0.0),
                        Object::Real(0.0),
                        Object::Real(612.0),
                        Object::Real(792.0),
                    ]),
                );
                d
            }),
        )
        .unwrap();
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));

        let pdf = w.write_to_vec().unwrap();
        assert!(pdf.starts_with(b"%PDF-"));
        assert!(pdf.ends_with(b"%%EOF\n"));
        assert!(bytes_contains(&pdf, b"xref"));
        assert!(bytes_contains(&pdf, b"startxref"));
        assert!(bytes_contains(&pdf, b"trailer"));
    }

    #[test]
    fn test_pdf_header_format() {
        let w = PdfWriter::with_version(1, 4);
        let pdf = w.write_to_vec().unwrap();
        assert!(pdf.starts_with(b"%PDF-1.4\n"));
        assert!(bytes_contains(&pdf, &[0x25, 0xE2, 0xE3, 0xCF, 0xD3])); // Binary comment
    }

    #[test]
    fn test_pdf_ends_with_eof() {
        let mut w = PdfWriter::new();
        w.add_object(1, 0, Object::Null).unwrap();
        w.set_trailer(Name::new("Size"), Object::Int(2));
        let pdf = w.write_to_vec().unwrap();
        assert!(pdf.ends_with(b"%%EOF\n"));
    }

    #[test]
    fn test_stream_compression() {
        let mut dict = Dict::new();
        // Use repetitive data that compresses well (short strings may not benefit from compression)
        let data: Vec<u8> = (0..500).map(|i| (i % 26) as u8 + b'a').collect();
        let stream = Object::Stream {
            dict: dict.clone(),
            data: data.clone(),
        };

        let mut buf_compressed = Vec::new();
        write_indirect_object(&mut buf_compressed, 1, 0, &stream, true).unwrap();
        assert!(bytes_contains(&buf_compressed, b"/FlateDecode"));
        assert!(bytes_contains(&buf_compressed, b"stream"));

        let mut buf_uncompressed = Vec::new();
        write_indirect_object(&mut buf_uncompressed, 1, 0, &stream, false).unwrap();
        assert!(!bytes_contains(&buf_uncompressed, b"/FlateDecode"));
        assert!(bytes_contains(&buf_uncompressed, &data));
    }

    #[test]
    fn test_stream_compression_skipped_when_larger() {
        // Short data: compression overhead can make output larger than input
        let mut dict = Dict::new();
        let data = b"Hi".to_vec();
        let stream = Object::Stream {
            dict: dict.clone(),
            data: data.clone(),
        };

        let mut buf = Vec::new();
        write_indirect_object(&mut buf, 1, 0, &stream, true).unwrap();
        // Should write uncompressed when compression doesn't reduce size
        assert!(!bytes_contains(&buf, b"/FlateDecode"));
        assert!(bytes_contains(&buf, b"Hi"));
    }

    #[test]
    fn test_stream_with_existing_filter_not_compressed() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Filter"), Object::Name(Name::new("DCTDecode")));
        let data = b"jpeg data".to_vec();
        let stream = Object::Stream { dict, data };

        let mut buf = Vec::new();
        write_indirect_object(&mut buf, 1, 0, &stream, true).unwrap();
        assert!(bytes_contains(&buf, b"DCTDecode"));
        assert!(bytes_contains(&buf, b"jpeg data"));
    }

    #[test]
    fn test_round_trip_structure() {
        let mut w = PdfWriter::new();
        w.add_object(
            1,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Catalog")));
                d.insert(Name::new("Pages"), Object::Ref(ObjRef::new(2, 0)));
                d
            }),
        )
        .unwrap();
        w.add_object(
            2,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Pages")));
                d.insert(Name::new("Count"), Object::Int(1));
                d.insert(
                    Name::new("Kids"),
                    Object::Array(vec![Object::Ref(ObjRef::new(3, 0))]),
                );
                d
            }),
        )
        .unwrap();
        w.add_object(
            3,
            0,
            Object::Dict({
                let mut d = Dict::new();
                d.insert(Name::new("Type"), Object::Name(Name::new("Page")));
                d.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
                d.insert(
                    Name::new("MediaBox"),
                    Object::Array(vec![
                        Object::Real(0.0),
                        Object::Real(0.0),
                        Object::Real(612.0),
                        Object::Real(792.0),
                    ]),
                );
                d
            }),
        )
        .unwrap();
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));

        let pdf = w.write_to_vec().unwrap();
        assert!(pdf.starts_with(b"%PDF-"));
        assert!(pdf.ends_with(b"%%EOF\n"));
        assert!(bytes_contains(&pdf, b"1 0 obj"));
        assert!(bytes_contains(&pdf, b"2 0 obj"));
        assert!(bytes_contains(&pdf, b"3 0 obj"));
        assert!(bytes_contains(&pdf, b"/Catalog"));
        assert!(bytes_contains(&pdf, b"/Pages"));
        assert!(bytes_contains(&pdf, b"/Page"));
    }

    #[test]
    fn test_write_options_default() {
        let opts = WriteOptions::default();
        assert!(opts.compress_streams);
        assert!(!opts.ascii_hex);
        assert!(!opts.incremental);
        assert!(!opts.linearize);
    }

    #[test]
    fn test_obj_zero_rejected() {
        let mut w = PdfWriter::new();
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        let result = w.add_object(0, 0, Object::Null);
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Object number 0 is reserved")
        );
    }

    #[test]
    fn test_duplicate_object_rejected() {
        let mut w = PdfWriter::new();
        w.add_object(1, 0, Object::Int(42)).unwrap();
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        let result = w.add_object(1, 0, Object::Int(99));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Duplicate object number")
        );
    }

    #[test]
    fn test_sparse_object_numbers_rejected() {
        let mut w = PdfWriter::new();
        w.add_object(1, 0, Object::Int(42)).unwrap();
        w.add_object(1 + MAX_OBJECT_GAP + 2, 0, Object::Int(99))
            .unwrap(); // gap of 1001 (objects 1 and 1003)
        w.set_trailer(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        let result = w.write_to_vec();
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Sparse object numbers not supported")
        );
    }

    #[test]
    fn test_xref_entry_exactly_20_bytes() {
        // PDF spec: each xref entry is exactly 20 bytes: "nnnnnnnnnn ggggg n\r\n"
        let entry = format!("{:010} {:05} {}\r\n", 0i64, 65535u16, "f");
        assert_eq!(
            entry.len(),
            20,
            "xref entry must be exactly 20 bytes per PDF spec"
        );
        let entry2 = format!("{:010} {:05} {}\r\n", 12345i64, 0u16, "n");
        assert_eq!(
            entry2.len(),
            20,
            "xref entry must be exactly 20 bytes per PDF spec"
        );
    }
}
