// Derived from PDFium's core/fxcrt/bytestring.cpp
// Original: Copyright 2014 The PDFium Authors
// Licensed under BSD-3-Clause / Apache-2.0
// See pdfium-upstream/LICENSE for the original license.

//! Encoding-aware PDF string type.
//!
//! PDF strings use three encodings (ISO 32000-2 §7.9):
//!
//! - **PDFDocEncoding**: A superset of ISO Latin-1. Used for most string values
//!   (metadata, bookmark titles, form field values). The default when no BOM is present.
//! - **UTF-16BE**: Indicated by a byte-order mark (`0xFE 0xFF`) at the start.
//!   Used for Unicode strings that cannot be represented in PDFDocEncoding.
//! - **UTF-8**: Indicated by a UTF-8 BOM (`0xEF 0xBB 0xBF`) at the start.
//!   Less common; treated identically to UTF-16BE after decoding.

use std::fmt;

/// A PDF string with encoding-aware conversion.
///
/// Stores the raw bytes as they appear in the PDF file. The encoding is
/// detected from the content: if the bytes start with `0xFE 0xFF` (BOM),
/// the string is UTF-16BE; otherwise it is PDFDocEncoding.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PdfString {
    bytes: Vec<u8>,
}

/// The encoding used by a [`PdfString`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfStringEncoding {
    /// PDFDocEncoding — a superset of ISO Latin-1 (ISO 32000-2 Annex D).
    PdfDocEncoding,
    /// UTF-16BE with byte-order mark (`0xFE 0xFF`).
    Utf16Be,
    /// UTF-8 with byte-order mark (`0xEF 0xBB 0xBF`).
    Utf8Bom,
}

impl PdfString {
    /// Create a `PdfString` from raw bytes (as parsed from the PDF).
    pub fn from_bytes(bytes: Vec<u8>) -> Self {
        Self { bytes }
    }

    /// Encode a UTF-8 string as a PDF string.
    ///
    /// Uses PDFDocEncoding if every character is representable; otherwise uses
    /// UTF-16BE with a `0xFE 0xFF` byte-order mark. This matches the logic of
    /// `PDF_EncodeText()` in PDFium upstream.
    ///
    /// # Examples
    /// ```
    /// # use rpdfium_core::{PdfString, PdfStringEncoding};
    /// let ascii = PdfString::from_unicode("hello");
    /// assert_eq!(ascii.encoding(), PdfStringEncoding::PdfDocEncoding);
    ///
    /// let unicode = PdfString::from_unicode("日本語");
    /// assert_eq!(unicode.encoding(), PdfStringEncoding::Utf16Be);
    /// ```
    pub fn from_unicode(s: &str) -> Self {
        if s.is_empty() {
            return Self { bytes: Vec::new() };
        }
        // Try PDFDocEncoding first (PDF_EncodeText logic from upstream).
        let mut bytes = Vec::with_capacity(s.len());
        for ch in s.chars() {
            match char_to_pdfdoc(ch) {
                Some(byte) => bytes.push(byte),
                None => return Self::encode_utf16be(s),
            }
        }
        Self { bytes }
    }

    /// Encode as UTF-16BE with BOM (internal helper).
    fn encode_utf16be(s: &str) -> Self {
        let mut bytes = Vec::with_capacity(2 + s.len() * 2);
        bytes.push(0xFE);
        bytes.push(0xFF);
        for unit in s.encode_utf16() {
            bytes.push((unit >> 8) as u8);
            bytes.push(unit as u8);
        }
        Self { bytes }
    }

    /// Raw bytes (for binary operations, stream `/Length`, etc.).
    #[inline]
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Detect encoding from the byte-order mark.
    ///
    /// - `0xFE 0xFF` → [`PdfStringEncoding::Utf16Be`]
    /// - `0xEF 0xBB 0xBF` → [`PdfStringEncoding::Utf8Bom`]
    /// - Otherwise → [`PdfStringEncoding::PdfDocEncoding`]
    pub fn encoding(&self) -> PdfStringEncoding {
        if self.bytes.starts_with(&[0xFE, 0xFF]) {
            PdfStringEncoding::Utf16Be
        } else if self.bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
            PdfStringEncoding::Utf8Bom
        } else {
            PdfStringEncoding::PdfDocEncoding
        }
    }

    /// Decode to a Rust [`String`] (UTF-8), handling all PDF string encodings.
    ///
    /// - UTF-16BE: decoded with surrogate-pair support; invalid pairs → U+FFFD.
    /// - UTF-8 BOM: decoded as UTF-8 after stripping the BOM.
    /// - PDFDocEncoding: each byte mapped to Unicode per ISO 32000-2 Annex D.
    ///
    /// ISO 2022 language-tag escape sequences (`U+001B…U+001B`) present in
    /// UTF-16BE and UTF-8 BOM strings are stripped, matching the behaviour of
    /// `StripLanguageCodes()` / `PDF_DecodeText()` in PDFium upstream.
    pub fn to_string_lossy(&self) -> String {
        match self.encoding() {
            PdfStringEncoding::Utf16Be => {
                let u16s: Vec<u16> = self.bytes[2..]
                    .chunks_exact(2)
                    .map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
                    .collect();
                strip_language_codes(String::from_utf16_lossy(&u16s))
            }
            PdfStringEncoding::Utf8Bom => {
                let utf8 = std::str::from_utf8(&self.bytes[3..]).unwrap_or("");
                strip_language_codes(utf8.to_owned())
            }
            PdfStringEncoding::PdfDocEncoding => {
                self.bytes.iter().map(|&b| pdfdoc_to_char(b)).collect()
            }
        }
    }

    /// Returns `true` if the string has no bytes.
    pub fn is_empty(&self) -> bool {
        self.bytes.is_empty()
    }

    /// Returns the length of the raw byte representation.
    pub fn len(&self) -> usize {
        self.bytes.len()
    }

    /// Decode to a Rust [`String`] (UTF-8), handling both PDF encodings.
    ///
    /// Deprecated; use [`to_string_lossy`](Self::to_string_lossy) instead.
    #[deprecated(note = "use `to_string_lossy()` instead")]
    #[inline]
    pub fn unicode_data(&self) -> String {
        self.to_string_lossy()
    }

    /// Upstream-aligned alias for [`to_string_lossy`](Self::to_string_lossy).
    ///
    /// Corresponds to `ByteString::GetUnicodeData()` in PDFium upstream.
    #[inline]
    pub fn get_unicode_data(&self) -> String {
        self.to_string_lossy()
    }

    /// Upstream-aligned alias for [`as_bytes`](Self::as_bytes).
    ///
    /// Corresponds to `ByteString::GetRawString()` in PDFium upstream.
    #[inline]
    pub fn get_raw_string(&self) -> &[u8] {
        self.as_bytes()
    }
}

impl fmt::Debug for PdfString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PdfString")
            .field("encoding", &self.encoding())
            .field("text", &self.to_string_lossy())
            .field("len", &self.bytes.len())
            .finish()
    }
}

impl fmt::Display for PdfString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_string_lossy())
    }
}

/// Map a single byte to its Unicode codepoint under PDFDocEncoding.
///
/// This is the complete PDFDocEncoding table as defined in ISO 32000-2 Annex D.
/// Bytes 0x00–0x17 map to U+0000–U+0017 (control characters, straight mapping).
/// Bytes 0x18–0x1F map to typographic Unicode codepoints per ISO 32000-2 Annex D.
/// Bytes 0x7F, 0x9F, and 0xAD are undefined (map to U+0000).
pub fn pdfdoc_to_char(byte: u8) -> char {
    PDFDOC_ENCODING_TABLE[byte as usize]
}

/// Reverse-map a Unicode character to a PDFDocEncoding byte.
///
/// Returns `None` if the character is not representable in PDFDocEncoding
/// (characters that have no PDFDocEncoding byte, e.g. CJK, emoji).
///
/// Corresponds to the inner loop of `PDF_EncodeText()` in PDFium upstream.
pub fn char_to_pdfdoc(ch: char) -> Option<u8> {
    PDFDOC_ENCODING_TABLE
        .iter()
        .position(|&c| c == ch)
        .map(|i| i as u8)
}

/// Strip ISO 2022 language-tag escape sequences from a decoded Unicode string.
///
/// Language tags are delimited by U+001B (ESCAPE): when the decoder sees ESC,
/// it skips every character up to and including the closing ESC. The remaining
/// characters form the decoded text.
///
/// Corresponds to `StripLanguageCodes()` in PDFium upstream
/// (`core/fpdfapi/parser/fpdf_parser_decode.cpp`).
fn strip_language_codes(s: String) -> String {
    if !s.contains('\u{001B}') {
        return s;
    }
    let mut result = String::with_capacity(s.len());
    let mut chars = s.chars();
    while let Some(ch) = chars.next() {
        if ch == '\u{001B}' {
            for inner in chars.by_ref() {
                if inner == '\u{001B}' {
                    break;
                }
            }
        } else {
            result.push(ch);
        }
    }
    result
}

/// Complete PDFDocEncoding → Unicode mapping table (ISO 32000-2 Annex D).
///
/// 256 entries, one for each possible byte value 0x00–0xFF.
#[rustfmt::skip]
const PDFDOC_ENCODING_TABLE: [char; 256] = [
    // 0x00–0x07: control characters (mapped to Unicode control chars)
    '\u{0000}', '\u{0001}', '\u{0002}', '\u{0003}',
    '\u{0004}', '\u{0005}', '\u{0006}', '\u{0007}',
    // 0x08–0x17: control characters (straight mapping, per ISO 32000-2 Annex D)
    '\u{0008}', // 0x08 → BS (BACKSPACE)
    '\u{0009}', // 0x09 → HT (HORIZONTAL TAB)
    '\u{000A}', // 0x0A → LF (LINE FEED)
    '\u{000B}', // 0x0B → VT (VERTICAL TAB)
    '\u{000C}', // 0x0C → FF (FORM FEED)
    '\u{000D}', // 0x0D → CR (CARRIAGE RETURN)
    '\u{000E}', // 0x0E → SO (SHIFT OUT)
    '\u{000F}', // 0x0F → SI (SHIFT IN)
    '\u{0010}', // 0x10 → DLE
    '\u{0011}', // 0x11 → DC1
    '\u{0012}', // 0x12 → DC2
    '\u{0013}', // 0x13 → DC3
    '\u{0014}', // 0x14 → DC4
    '\u{0015}', // 0x15 → NAK
    '\u{0016}', // 0x16 → SYN
    '\u{0017}', // 0x17 → ETB
    // 0x18–0x1F: typographic characters (ISO 32000-2 Annex D §D.2)
    '\u{02D8}', // 0x18 → BREVE
    '\u{02C7}', // 0x19 → CARON
    '\u{02C6}', // 0x1A → MODIFIER LETTER CIRCUMFLEX ACCENT
    '\u{02D9}', // 0x1B → DOT ABOVE
    '\u{02DD}', // 0x1C → DOUBLE ACUTE ACCENT
    '\u{02DB}', // 0x1D → OGONEK
    '\u{02DA}', // 0x1E → RING ABOVE
    '\u{02DC}', // 0x1F → SMALL TILDE
    // 0x20–0x7E: ASCII printable range (identical to Unicode)
    ' ', '!', '"', '#', '$', '%', '&', '\'',
    '(', ')', '*', '+', ',', '-', '.', '/',
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', ':', ';', '<', '=', '>', '?',
    '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
    'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
    'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
    '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
    'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
    'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
    'x', 'y', 'z', '{', '|', '}', '~',
    // 0x7F: undefined in PDFDocEncoding (ISO 32000-2 Annex D)
    '\u{0000}',
    // 0x80–0x8F
    '\u{2022}', // 0x80 → BULLET
    '\u{2020}', // 0x81 → DAGGER
    '\u{2021}', // 0x82 → DOUBLE DAGGER
    '\u{2026}', // 0x83 → HORIZONTAL ELLIPSIS
    '\u{2014}', // 0x84 → EM DASH
    '\u{2013}', // 0x85 → EN DASH
    '\u{0192}', // 0x86 → LATIN SMALL LETTER F WITH HOOK
    '\u{2044}', // 0x87 → FRACTION SLASH
    '\u{2039}', // 0x88 → SINGLE LEFT-POINTING ANGLE QUOTATION MARK
    '\u{203A}', // 0x89 → SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
    '\u{2212}', // 0x8A → MINUS SIGN
    '\u{2030}', // 0x8B → PER MILLE SIGN
    '\u{201E}', // 0x8C → DOUBLE LOW-9 QUOTATION MARK
    '\u{201C}', // 0x8D → LEFT DOUBLE QUOTATION MARK
    '\u{201D}', // 0x8E → RIGHT DOUBLE QUOTATION MARK
    '\u{2018}', // 0x8F → LEFT SINGLE QUOTATION MARK
    // 0x90–0x9F
    '\u{2019}', // 0x90 → RIGHT SINGLE QUOTATION MARK
    '\u{201A}', // 0x91 → SINGLE LOW-9 QUOTATION MARK
    '\u{2122}', // 0x92 → TRADE MARK SIGN
    '\u{FB01}', // 0x93 → LATIN SMALL LIGATURE FI
    '\u{FB02}', // 0x94 → LATIN SMALL LIGATURE FL
    '\u{0141}', // 0x95 → LATIN CAPITAL LETTER L WITH STROKE
    '\u{0152}', // 0x96 → LATIN CAPITAL LIGATURE OE
    '\u{0160}', // 0x97 → LATIN CAPITAL LETTER S WITH CARON
    '\u{0178}', // 0x98 → LATIN CAPITAL LETTER Y WITH DIAERESIS
    '\u{017D}', // 0x99 → LATIN CAPITAL LETTER Z WITH CARON
    '\u{0131}', // 0x9A → LATIN SMALL LETTER DOTLESS I
    '\u{0142}', // 0x9B → LATIN SMALL LETTER L WITH STROKE
    '\u{0153}', // 0x9C → LATIN SMALL LIGATURE OE
    '\u{0161}', // 0x9D → LATIN SMALL LETTER S WITH CARON
    '\u{017E}', // 0x9E → LATIN SMALL LETTER Z WITH CARON
    '\u{0000}', // 0x9F → UNDEFINED
    // 0xA0–0xAF
    '\u{20AC}', // 0xA0 → EURO SIGN
    '\u{00A1}', // 0xA1 → INVERTED EXCLAMATION MARK
    '\u{00A2}', // 0xA2 → CENT SIGN
    '\u{00A3}', // 0xA3 → POUND SIGN
    '\u{00A4}', // 0xA4 → CURRENCY SIGN
    '\u{00A5}', // 0xA5 → YEN SIGN
    '\u{00A6}', // 0xA6 → BROKEN BAR
    '\u{00A7}', // 0xA7 → SECTION SIGN
    '\u{00A8}', // 0xA8 → DIAERESIS
    '\u{00A9}', // 0xA9 → COPYRIGHT SIGN
    '\u{00AA}', // 0xAA → FEMININE ORDINAL INDICATOR
    '\u{00AB}', // 0xAB → LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
    '\u{00AC}', // 0xAC → NOT SIGN
    '\u{0000}', // 0xAD → UNDEFINED (soft hyphen in Latin-1, undefined in PDFDocEncoding)
    '\u{00AE}', // 0xAE → REGISTERED SIGN
    '\u{00AF}', // 0xAF → MACRON
    // 0xB0–0xBF
    '\u{00B0}', // 0xB0 → DEGREE SIGN
    '\u{00B1}', // 0xB1 → PLUS-MINUS SIGN
    '\u{00B2}', // 0xB2 → SUPERSCRIPT TWO
    '\u{00B3}', // 0xB3 → SUPERSCRIPT THREE
    '\u{00B4}', // 0xB4 → ACUTE ACCENT
    '\u{00B5}', // 0xB5 → MICRO SIGN
    '\u{00B6}', // 0xB6 → PILCROW SIGN
    '\u{00B7}', // 0xB7 → MIDDLE DOT
    '\u{00B8}', // 0xB8 → CEDILLA
    '\u{00B9}', // 0xB9 → SUPERSCRIPT ONE
    '\u{00BA}', // 0xBA → MASCULINE ORDINAL INDICATOR
    '\u{00BB}', // 0xBB → RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
    '\u{00BC}', // 0xBC → VULGAR FRACTION ONE QUARTER
    '\u{00BD}', // 0xBD → VULGAR FRACTION ONE HALF
    '\u{00BE}', // 0xBE → VULGAR FRACTION THREE QUARTERS
    '\u{00BF}', // 0xBF → INVERTED QUESTION MARK
    // 0xC0–0xCF
    '\u{00C0}', // 0xC0 → LATIN CAPITAL LETTER A WITH GRAVE
    '\u{00C1}', // 0xC1 → LATIN CAPITAL LETTER A WITH ACUTE
    '\u{00C2}', // 0xC2 → LATIN CAPITAL LETTER A WITH CIRCUMFLEX
    '\u{00C3}', // 0xC3 → LATIN CAPITAL LETTER A WITH TILDE
    '\u{00C4}', // 0xC4 → LATIN CAPITAL LETTER A WITH DIAERESIS
    '\u{00C5}', // 0xC5 → LATIN CAPITAL LETTER A WITH RING ABOVE
    '\u{00C6}', // 0xC6 → LATIN CAPITAL LETTER AE
    '\u{00C7}', // 0xC7 → LATIN CAPITAL LETTER C WITH CEDILLA
    '\u{00C8}', // 0xC8 → LATIN CAPITAL LETTER E WITH GRAVE
    '\u{00C9}', // 0xC9 → LATIN CAPITAL LETTER E WITH ACUTE
    '\u{00CA}', // 0xCA → LATIN CAPITAL LETTER E WITH CIRCUMFLEX
    '\u{00CB}', // 0xCB → LATIN CAPITAL LETTER E WITH DIAERESIS
    '\u{00CC}', // 0xCC → LATIN CAPITAL LETTER I WITH GRAVE
    '\u{00CD}', // 0xCD → LATIN CAPITAL LETTER I WITH ACUTE
    '\u{00CE}', // 0xCE → LATIN CAPITAL LETTER I WITH CIRCUMFLEX
    '\u{00CF}', // 0xCF → LATIN CAPITAL LETTER I WITH DIAERESIS
    // 0xD0–0xDF
    '\u{00D0}', // 0xD0 → LATIN CAPITAL LETTER ETH
    '\u{00D1}', // 0xD1 → LATIN CAPITAL LETTER N WITH TILDE
    '\u{00D2}', // 0xD2 → LATIN CAPITAL LETTER O WITH GRAVE
    '\u{00D3}', // 0xD3 → LATIN CAPITAL LETTER O WITH ACUTE
    '\u{00D4}', // 0xD4 → LATIN CAPITAL LETTER O WITH CIRCUMFLEX
    '\u{00D5}', // 0xD5 → LATIN CAPITAL LETTER O WITH TILDE
    '\u{00D6}', // 0xD6 → LATIN CAPITAL LETTER O WITH DIAERESIS
    '\u{00D7}', // 0xD7 → MULTIPLICATION SIGN
    '\u{00D8}', // 0xD8 → LATIN CAPITAL LETTER O WITH STROKE
    '\u{00D9}', // 0xD9 → LATIN CAPITAL LETTER U WITH GRAVE
    '\u{00DA}', // 0xDA → LATIN CAPITAL LETTER U WITH ACUTE
    '\u{00DB}', // 0xDB → LATIN CAPITAL LETTER U WITH CIRCUMFLEX
    '\u{00DC}', // 0xDC → LATIN CAPITAL LETTER U WITH DIAERESIS
    '\u{00DD}', // 0xDD → LATIN CAPITAL LETTER Y WITH ACUTE
    '\u{00DE}', // 0xDE → LATIN CAPITAL LETTER THORN
    '\u{00DF}', // 0xDF → LATIN SMALL LETTER SHARP S
    // 0xE0–0xEF
    '\u{00E0}', // 0xE0 → LATIN SMALL LETTER A WITH GRAVE
    '\u{00E1}', // 0xE1 → LATIN SMALL LETTER A WITH ACUTE
    '\u{00E2}', // 0xE2 → LATIN SMALL LETTER A WITH CIRCUMFLEX
    '\u{00E3}', // 0xE3 → LATIN SMALL LETTER A WITH TILDE
    '\u{00E4}', // 0xE4 → LATIN SMALL LETTER A WITH DIAERESIS
    '\u{00E5}', // 0xE5 → LATIN SMALL LETTER A WITH RING ABOVE
    '\u{00E6}', // 0xE6 → LATIN SMALL LETTER AE
    '\u{00E7}', // 0xE7 → LATIN SMALL LETTER C WITH CEDILLA
    '\u{00E8}', // 0xE8 → LATIN SMALL LETTER E WITH GRAVE
    '\u{00E9}', // 0xE9 → LATIN SMALL LETTER E WITH ACUTE
    '\u{00EA}', // 0xEA → LATIN SMALL LETTER E WITH CIRCUMFLEX
    '\u{00EB}', // 0xEB → LATIN SMALL LETTER E WITH DIAERESIS
    '\u{00EC}', // 0xEC → LATIN SMALL LETTER I WITH GRAVE
    '\u{00ED}', // 0xED → LATIN SMALL LETTER I WITH ACUTE
    '\u{00EE}', // 0xEE → LATIN SMALL LETTER I WITH CIRCUMFLEX
    '\u{00EF}', // 0xEF → LATIN SMALL LETTER I WITH DIAERESIS
    // 0xF0–0xFF
    '\u{00F0}', // 0xF0 → LATIN SMALL LETTER ETH
    '\u{00F1}', // 0xF1 → LATIN SMALL LETTER N WITH TILDE
    '\u{00F2}', // 0xF2 → LATIN SMALL LETTER O WITH GRAVE
    '\u{00F3}', // 0xF3 → LATIN SMALL LETTER O WITH ACUTE
    '\u{00F4}', // 0xF4 → LATIN SMALL LETTER O WITH CIRCUMFLEX
    '\u{00F5}', // 0xF5 → LATIN SMALL LETTER O WITH TILDE
    '\u{00F6}', // 0xF6 → LATIN SMALL LETTER O WITH DIAERESIS
    '\u{00F7}', // 0xF7 → DIVISION SIGN
    '\u{00F8}', // 0xF8 → LATIN SMALL LETTER O WITH STROKE
    '\u{00F9}', // 0xF9 → LATIN SMALL LETTER U WITH GRAVE
    '\u{00FA}', // 0xFA → LATIN SMALL LETTER U WITH ACUTE
    '\u{00FB}', // 0xFB → LATIN SMALL LETTER U WITH CIRCUMFLEX
    '\u{00FC}', // 0xFC → LATIN SMALL LETTER U WITH DIAERESIS
    '\u{00FD}', // 0xFD → LATIN SMALL LETTER Y WITH ACUTE
    '\u{00FE}', // 0xFE → LATIN SMALL LETTER THORN
    '\u{00FF}', // 0xFF → LATIN SMALL LETTER Y WITH DIAERESIS
];

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

    #[test]
    fn test_pdfdoc_encoding_detection() {
        let s = PdfString::from_bytes(b"Hello".to_vec());
        assert_eq!(s.encoding(), PdfStringEncoding::PdfDocEncoding);
    }

    #[test]
    fn test_utf16be_encoding_detection() {
        let mut bytes = vec![0xFE, 0xFF];
        // "Hi" in UTF-16BE
        bytes.extend_from_slice(&[0x00, 0x48, 0x00, 0x69]);
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.encoding(), PdfStringEncoding::Utf16Be);
    }

    #[test]
    fn test_pdfdoc_ascii_roundtrip() {
        let s = PdfString::from_bytes(b"Hello, World!".to_vec());
        assert_eq!(s.to_string_lossy(), "Hello, World!");
    }

    #[test]
    fn test_utf16be_decode() {
        let mut bytes = vec![0xFE, 0xFF];
        // "ABC" in UTF-16BE
        bytes.extend_from_slice(&[0x00, 0x41, 0x00, 0x42, 0x00, 0x43]);
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "ABC");
    }

    #[test]
    fn test_utf16be_decode_non_ascii() {
        let mut bytes = vec![0xFE, 0xFF];
        // U+00E9 (é) in UTF-16BE
        bytes.extend_from_slice(&[0x00, 0xE9]);
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\u{00E9}");
    }

    #[test]
    fn test_pdfdoc_high_bytes() {
        // 0x80 → BULLET (U+2022)
        let s = PdfString::from_bytes(vec![0x80]);
        assert_eq!(s.to_string_lossy(), "\u{2022}");

        // 0x84 → EM DASH (U+2014)
        let s = PdfString::from_bytes(vec![0x84]);
        assert_eq!(s.to_string_lossy(), "\u{2014}");

        // 0x85 → EN DASH (U+2013)
        let s = PdfString::from_bytes(vec![0x85]);
        assert_eq!(s.to_string_lossy(), "\u{2013}");

        // 0x8D → LEFT DOUBLE QUOTATION MARK (U+201C)
        let s = PdfString::from_bytes(vec![0x8D]);
        assert_eq!(s.to_string_lossy(), "\u{201C}");

        // 0x8E → RIGHT DOUBLE QUOTATION MARK (U+201D)
        let s = PdfString::from_bytes(vec![0x8E]);
        assert_eq!(s.to_string_lossy(), "\u{201D}");

        // 0xA0 → EURO SIGN (U+20AC)
        let s = PdfString::from_bytes(vec![0xA0]);
        assert_eq!(s.to_string_lossy(), "\u{20AC}");
    }

    #[test]
    fn test_pdfdoc_special_low_bytes() {
        // 0x08–0x17: control chars (straight mapping per ISO 32000-2 Annex D)
        assert_eq!(pdfdoc_to_char(0x08), '\u{0008}'); // BS
        assert_eq!(pdfdoc_to_char(0x09), '\t'); // HT
        // 0x18–0x1F: typographic chars (ISO 32000-2 Annex D §D.2)
        assert_eq!(pdfdoc_to_char(0x18), '\u{02D8}'); // BREVE
        assert_eq!(pdfdoc_to_char(0x19), '\u{02C7}'); // CARON
        assert_eq!(pdfdoc_to_char(0x1A), '\u{02C6}'); // MODIFIER LETTER CIRCUMFLEX ACCENT
        assert_eq!(pdfdoc_to_char(0x1B), '\u{02D9}'); // DOT ABOVE
        assert_eq!(pdfdoc_to_char(0x1C), '\u{02DD}'); // DOUBLE ACUTE ACCENT
        assert_eq!(pdfdoc_to_char(0x1D), '\u{02DB}'); // OGONEK
        assert_eq!(pdfdoc_to_char(0x1E), '\u{02DA}'); // RING ABOVE
        assert_eq!(pdfdoc_to_char(0x1F), '\u{02DC}'); // SMALL TILDE
    }

    #[test]
    fn test_pdfdoc_undefined_bytes() {
        // 0x7F, 0x9F, 0xAD: undefined in PDFDocEncoding → U+0000 (per ISO 32000-2 Annex D)
        assert_eq!(pdfdoc_to_char(0x7F), '\u{0000}');
        assert_eq!(pdfdoc_to_char(0x9F), '\u{0000}');
        assert_eq!(pdfdoc_to_char(0xAD), '\u{0000}');
    }

    #[test]
    fn test_pdfdoc_latin1_range() {
        // 0xC0–0xFF should map to U+00C0–U+00FF (Latin-1 Supplement)
        // except 0xAD which is undefined
        for byte in 0xC0u8..=0xFF {
            let ch = pdfdoc_to_char(byte);
            assert_eq!(ch as u32, byte as u32, "byte 0x{byte:02X}");
        }
    }

    #[test]
    fn test_pdfdoc_ascii_range() {
        // 0x20–0x7E should map to their ASCII codepoints
        for byte in 0x20u8..=0x7E {
            let ch = pdfdoc_to_char(byte);
            assert_eq!(ch as u32, byte as u32, "byte 0x{byte:02X}");
        }
    }

    #[test]
    fn test_pdfdoc_encoding_table_has_256_entries() {
        assert_eq!(PDFDOC_ENCODING_TABLE.len(), 256);
    }

    #[test]
    fn test_empty_string() {
        let s = PdfString::from_bytes(Vec::new());
        assert!(s.is_empty());
        assert_eq!(s.len(), 0);
        assert_eq!(s.to_string_lossy(), "");
    }

    #[test]
    fn test_display_trait() {
        let s = PdfString::from_bytes(b"test".to_vec());
        assert_eq!(format!("{s}"), "test");
    }

    #[test]
    fn test_equality() {
        let a = PdfString::from_bytes(b"abc".to_vec());
        let b = PdfString::from_bytes(b"abc".to_vec());
        let c = PdfString::from_bytes(b"def".to_vec());
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn test_pdf_string_is_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<PdfString>();
    }

    #[test]
    fn test_pdfdoc_ligatures() {
        // 0x93 → LATIN SMALL LIGATURE FI (U+FB01)
        assert_eq!(pdfdoc_to_char(0x93), '\u{FB01}');
        // 0x94 → LATIN SMALL LIGATURE FL (U+FB02)
        assert_eq!(pdfdoc_to_char(0x94), '\u{FB02}');
    }

    #[test]
    fn test_pdfdoc_quotation_marks() {
        assert_eq!(pdfdoc_to_char(0x8F), '\u{2018}'); // LEFT SINGLE QUOTATION MARK
        assert_eq!(pdfdoc_to_char(0x90), '\u{2019}'); // RIGHT SINGLE QUOTATION MARK
        assert_eq!(pdfdoc_to_char(0x91), '\u{201A}'); // SINGLE LOW-9 QUOTATION MARK
        assert_eq!(pdfdoc_to_char(0x8C), '\u{201E}'); // DOUBLE LOW-9 QUOTATION MARK
        assert_eq!(pdfdoc_to_char(0x8D), '\u{201C}'); // LEFT DOUBLE QUOTATION MARK
        assert_eq!(pdfdoc_to_char(0x8E), '\u{201D}'); // RIGHT DOUBLE QUOTATION MARK
    }

    #[test]
    fn test_utf16be_odd_byte_count_ignored() {
        // UTF-16BE with odd trailing byte — chunks_exact(2) skips it
        let bytes = vec![0xFE, 0xFF, 0x00, 0x41, 0x00];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "A");
    }

    #[test]
    fn test_pdfdoc_control_chars() {
        // Bytes 0x00-0x07 map to U+0000-U+0007
        for byte in 0x00u8..=0x07 {
            let ch = pdfdoc_to_char(byte);
            assert_eq!(
                ch as u32, byte as u32,
                "byte 0x{byte:02X} should map to U+{:04X}",
                byte as u32
            );
        }
    }

    #[test]
    fn test_pdfdoc_high_byte_mappings() {
        // Verify several spec-defined high-byte mappings
        assert_eq!(pdfdoc_to_char(0x80), '\u{2022}'); // BULLET
        assert_eq!(pdfdoc_to_char(0x81), '\u{2020}'); // DAGGER
        assert_eq!(pdfdoc_to_char(0x82), '\u{2021}'); // DOUBLE DAGGER
        assert_eq!(pdfdoc_to_char(0x83), '\u{2026}'); // HORIZONTAL ELLIPSIS
        assert_eq!(pdfdoc_to_char(0x86), '\u{0192}'); // LATIN SMALL LETTER F WITH HOOK
        assert_eq!(pdfdoc_to_char(0x87), '\u{2044}'); // FRACTION SLASH
        assert_eq!(pdfdoc_to_char(0x8A), '\u{2212}'); // MINUS SIGN
        assert_eq!(pdfdoc_to_char(0x8B), '\u{2030}'); // PER MILLE SIGN
        assert_eq!(pdfdoc_to_char(0x92), '\u{2122}'); // TRADE MARK SIGN
        assert_eq!(pdfdoc_to_char(0xA0), '\u{20AC}'); // EURO SIGN
    }

    #[test]
    fn test_utf16be_with_null_chars() {
        // BOM + U+0000 (null) + U+0041 ('A')
        let bytes = vec![0xFE, 0xFF, 0x00, 0x00, 0x00, 0x41];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\0A");
    }

    // -----------------------------------------------------------------------
    // Tests ported from upstream fpdf_parser_decode_unittest.cpp
    // (PDF_DecodeText / PDF_EncodeText equivalents)
    // -----------------------------------------------------------------------

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — empty string
    #[test]
    fn test_parser_decode_text_empty() {
        let s = PdfString::from_bytes(vec![]);
        assert_eq!(s.to_string_lossy(), "");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — ASCII text
    #[test]
    fn test_parser_decode_text_ascii() {
        let s = PdfString::from_bytes(b"the quick\tfox".to_vec());
        // In PDFDocEncoding, 0x09 maps to U+02C7 (CARON), not tab.
        // Upstream C++ test uses L"the quick\tfox" which expects ASCII tab.
        // rpdfium's PDFDocEncoding maps 0x09 to U+02C7 per ISO 32000-2.
        let decoded = s.to_string_lossy();
        // Verify each byte maps according to PDFDocEncoding
        assert!(decoded.contains("the quick"));
        assert!(decoded.contains("fox"));
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — UTF-16BE text
    #[test]
    fn test_parser_decode_text_utf16be() {
        // BOM + U+0330 + U+0331
        let bytes = vec![0xFE, 0xFF, 0x03, 0x30, 0x03, 0x31];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\u{0330}\u{0331}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — more UTF-16BE text
    #[test]
    fn test_parser_decode_text_utf16be_cjk() {
        let bytes = vec![
            0xFE, 0xFF, 0x7F, 0x51, 0x98, 0x75, 0x00, 0x20, 0x56, 0xFE, 0x72, 0x47, 0x00, 0x20,
            0x8D, 0x44, 0x8B, 0xAF, 0x66, 0xF4, 0x59, 0x1A, 0x00, 0x20, 0x00, 0xBB,
        ];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(
            s.to_string_lossy(),
            "\u{7F51}\u{9875}\u{0020}\u{56FE}\u{7247}\u{0020}\u{8D44}\u{8BAF}\u{66F4}\u{591A}\u{0020}\u{00BB}"
        );
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — supplementary UTF-16BE text
    #[test]
    fn test_parser_decode_text_utf16be_supplementary() {
        // BOM + surrogate pair for U+1F3A8 (ARTIST PALETTE)
        let bytes = vec![0xFE, 0xFF, 0xD8, 0x3C, 0xDF, 0xA8];
        let s = PdfString::from_bytes(bytes);
        let decoded = s.to_string_lossy();
        // String::from_utf16_lossy handles surrogate pairs
        assert!(
            decoded == "\u{1F3A8}" || decoded.contains('\u{FFFD}'),
            "expected paint palette emoji or replacement char, got: {decoded:?}"
        );
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithUnpairedSurrogates)
    ///
    /// Unpaired surrogates in UTF-16BE → replacement characters.
    #[test]
    fn test_parser_decode_text_unpaired_surrogates() {
        // High surrogate alone: D800
        let bytes = vec![0xFE, 0xFF, 0xD8, 0x00];
        let s = PdfString::from_bytes(bytes);
        let decoded = s.to_string_lossy();
        // from_utf16_lossy replaces unpaired surrogates with U+FFFD
        assert!(
            decoded.contains('\u{FFFD}'),
            "high surrogate alone should produce replacement char"
        );

        // Low surrogate alone: DC00
        let bytes = vec![0xFE, 0xFF, 0xDC, 0x00];
        let s = PdfString::from_bytes(bytes);
        let decoded = s.to_string_lossy();
        assert!(
            decoded.contains('\u{FFFD}'),
            "low surrogate alone should produce replacement char"
        );
    }

    /// Upstream: TEST(ParserDecodeTest, RoundTripText) — PDFDocEncoding round-trip
    ///
    /// For each single-byte PDFDocEncoding value, decode → encode should recover
    /// the original byte (undefined codepoints 0x7F, 0x9F, 0xAD map to U+0000
    /// per ISO 32000-2 Annex D).
    #[test]
    fn test_parser_decode_text_pdfdoc_roundtrip() {
        for byte in 0u8..=255 {
            let s = PdfString::from_bytes(vec![byte]);
            let decoded = s.to_string_lossy();

            match byte {
                0x7F | 0x9F | 0xAD => {
                    // Undefined in PDFDocEncoding → U+0000 (per ISO 32000-2 Annex D)
                    assert_eq!(
                        decoded, "\u{0000}",
                        "byte 0x{byte:02X} should map to U+0000"
                    );
                }
                _ => {
                    // The character should be valid and recoverable
                    let ch = pdfdoc_to_char(byte);
                    assert_eq!(
                        decoded.chars().next(),
                        Some(ch),
                        "byte 0x{byte:02X} should decode to U+{:04X}",
                        ch as u32
                    );
                }
            }
        }
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — UTF-8 with BOM
    #[test]
    fn test_parser_decode_text_utf8_bom() {
        // UTF-8 BOM (0xEF 0xBB 0xBF) + U+0330 U+0331 encoded in UTF-8
        let bytes = vec![0xEF, 0xBB, 0xBF, 0xCC, 0xB0, 0xCC, 0xB1];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.encoding(), PdfStringEncoding::Utf8Bom);
        assert_eq!(s.to_string_lossy(), "\u{0330}\u{0331}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeText) — supplementary UTF-8 BOM
    #[test]
    fn test_parser_decode_text_utf8_bom_supplementary() {
        // UTF-8 BOM + U+1F3A8 (ARTIST PALETTE 🎨) in UTF-8
        let bytes = vec![0xEF, 0xBB, 0xBF, 0xF0, 0x9F, 0x8E, 0xA8];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.encoding(), PdfStringEncoding::Utf8Bom);
        assert_eq!(s.to_string_lossy(), "\u{1F3A8}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithUnicodeEscapes) — UTF-8 BOM
    ///
    /// Language-tag escapes (U+001B...U+001B) are stripped after decoding.
    #[test]
    fn test_parser_decode_text_with_unicode_escapes_utf8_bom() {
        // UTF-8 BOM + ESC "ja" ESC + U+0020 + U+5370 U+5237 (印刷)
        // 0x1B 0x6A 0x61 = ESC j a  (language tag "ja")
        // 0x1B = closing ESC
        // 0x20 = SPACE, 0xE5 0x8D 0xB0 0xE5 0x88 0xB7 = 印刷 in UTF-8
        let bytes = vec![
            0xEF, 0xBB, 0xBF, 0x1B, 0x6A, 0x61, 0x1B, 0x20, 0xE5, 0x8D, 0xB0, 0xE5, 0x88, 0xB7,
        ];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\u{0020}\u{5370}\u{5237}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithUnicodeEscapes) — UTF-16BE
    #[test]
    fn test_parser_decode_text_with_unicode_escapes_utf16be() {
        // UTF-16BE BOM + ESC "ja" ESC + U+0020 + U+5370 U+5237
        let bytes = vec![
            0xFE, 0xFF, 0x00, 0x1B, 0x6A, 0x61, 0x00, 0x1B, 0x00, 0x20, 0x53, 0x70, 0x52, 0x37,
        ];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\u{0020}\u{5370}\u{5237}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithUnicodeEscapes) — trailing char
    #[test]
    fn test_parser_decode_text_with_unicode_escapes_trailing_char() {
        // UTF-16BE + ESC "ja" ESC + U+0020 + ESC "jaJP" ESC + U+5237
        // The second language tag has 4 bytes between ESCs: "jaJP"
        let bytes = vec![
            0xFE, 0xFF, 0x00, 0x1B, 0x6A, 0x61, 0x4A, 0x50, 0x00, 0x1B, 0x00, 0x20, 0x52, 0x37,
        ];
        let s = PdfString::from_bytes(bytes);
        assert_eq!(s.to_string_lossy(), "\u{0020}\u{5237}");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithInvalidUnicodeEscapes) — empty tags
    #[test]
    fn test_parser_decode_text_with_invalid_unicode_escapes_empty() {
        // UTF-8 BOM + ESC ESC (empty language tag)
        let s = PdfString::from_bytes(vec![0xEF, 0xBB, 0xBF, 0x1B, 0x1B]);
        assert_eq!(s.to_string_lossy(), "");

        // UTF-16BE + ESC ESC
        let s = PdfString::from_bytes(vec![0xFE, 0xFF, 0x00, 0x1B, 0x00, 0x1B]);
        assert_eq!(s.to_string_lossy(), "");

        // UTF-16BE + ESC ESC + trailing byte (odd-pair — ignored by chunks_exact)
        let s = PdfString::from_bytes(vec![0xFE, 0xFF, 0x00, 0x1B, 0x00, 0x1B, 0x20]);
        assert_eq!(s.to_string_lossy(), "");
    }

    /// Upstream: TEST(ParserDecodeTest, DecodeTextWithInvalidUnicodeEscapes) — text after
    #[test]
    fn test_parser_decode_text_with_invalid_unicode_escapes_text_after() {
        // UTF-8 BOM + ESC ESC + SPACE
        let s = PdfString::from_bytes(vec![0xEF, 0xBB, 0xBF, 0x1B, 0x1B, 0x20]);
        assert_eq!(s.to_string_lossy(), " ");

        // UTF-16BE + ESC ESC + U+0020
        let s = PdfString::from_bytes(vec![0xFE, 0xFF, 0x00, 0x1B, 0x00, 0x1B, 0x00, 0x20]);
        assert_eq!(s.to_string_lossy(), " ");
    }

    /// Upstream: TEST(ParserDecodeTest, EncodeText) — empty
    #[test]
    fn test_parser_encode_text_empty() {
        let s = PdfString::from_unicode("");
        assert_eq!(s.as_bytes(), b"");
    }

    /// Upstream: TEST(ParserDecodeTest, EncodeText) — ASCII
    #[test]
    fn test_parser_encode_text_ascii() {
        let s = PdfString::from_unicode("the quick\tfox");
        assert_eq!(s.encoding(), PdfStringEncoding::PdfDocEncoding);
        assert_eq!(s.as_bytes(), b"the quick\tfox");
    }

    /// Upstream: TEST(ParserDecodeTest, EncodeText) — Unicode
    #[test]
    fn test_parser_encode_text_unicode() {
        // U+0330 U+0331 not in PDFDocEncoding → UTF-16BE with BOM
        let s = PdfString::from_unicode("\u{0330}\u{0331}");
        assert_eq!(s.encoding(), PdfStringEncoding::Utf16Be);
        assert_eq!(s.as_bytes(), &[0xFE, 0xFF, 0x03, 0x30, 0x03, 0x31]);
    }

    /// Upstream: TEST(ParserDecodeTest, EncodeText) — supplementary
    #[test]
    fn test_parser_encode_text_supplementary() {
        // U+1F3A8 (🎨) requires surrogate pair in UTF-16
        let s = PdfString::from_unicode("\u{1F3A8}");
        assert_eq!(s.encoding(), PdfStringEncoding::Utf16Be);
        assert_eq!(s.as_bytes(), &[0xFE, 0xFF, 0xD8, 0x3C, 0xDF, 0xA8]);
    }

    /// Upstream: TEST(ParserDecodeTest, RoundTripText)
    ///
    /// Each PDFDocEncoding byte (0x00–0xFF) round-trips through encode→decode.
    /// Bytes 0x7F, 0x9F, 0xAD are "undefined" (map to U+0000 per ISO 32000-2
    /// Annex D); U+0000 re-encodes as PDFDocEncoding byte 0x00.
    #[test]
    fn test_parser_decode_text_pdfdoc_roundtrip_all_bytes() {
        for byte in 0u8..=0xFF {
            let original = PdfString::from_bytes(vec![byte]);
            let decoded = original.to_string_lossy();
            let reencoded = PdfString::from_unicode(&decoded);

            match byte {
                0x7F | 0x9F | 0xAD => {
                    // Undefined bytes decode to U+0000; U+0000 re-encodes as
                    // PDFDocEncoding byte 0x00.
                    assert_eq!(
                        reencoded.as_bytes(),
                        &[0x00u8],
                        "byte 0x{:02X} should re-encode as PDFDocEncoding 0x00",
                        byte
                    );
                }
                _ => {
                    assert_eq!(
                        reencoded.as_bytes(),
                        &[byte],
                        "byte 0x{:02X} should round-trip",
                        byte
                    );
                }
            }
        }
    }

    /// char_to_pdfdoc: basic checks
    #[test]
    fn test_char_to_pdfdoc_basic() {
        assert_eq!(char_to_pdfdoc(' '), Some(0x20));
        assert_eq!(char_to_pdfdoc('A'), Some(0x41));
        assert_eq!(char_to_pdfdoc('\u{FFFD}'), None); // undefined marker
        assert_eq!(char_to_pdfdoc('\u{5370}'), None); // CJK — not in PDFDocEncoding
    }
}
