//! Codec layer — format detection, decoding, and the explicit anti-list.
//!
//! Architecture:
//! - [`FormatRegistry`] holds the list of supported formats and answers
//!   "is this file supported?" queries.
//! - [`Codec`] trait — every decoder implements this.
//! - [`anti_list`] module owns the list of formats we deliberately reject,
//!   with clear user-facing reasons.
//!
//! Decoding flow:
//! 1. Caller passes a path to `FormatRegistry::decode(path)`.
//! 2. Registry checks the extension against the anti-list first. If it matches,
//!    returns [`DecodeError::AntiListed`] immediately — no I/O, no decode attempt.
//! 3. Registry checks the extension against the supported-formats list. If it
//!    matches, hands off to the appropriate codec.
//! 4. Codec reads the file, decodes, returns a [`DecodedImage`].
//! 5. If the extension is unknown (not in anti-list, not in supported list),
//!    returns [`DecodeError::Unsupported`].
//!
//! This layer is intentionally synchronous. Async wrapping happens at the
//! `app` layer via `iced::Task::perform`, which keeps the codec code simple
//! and testable.

pub mod anti_list;
pub mod exr;
pub mod image_crate;
pub mod jxl;
pub mod svg;
pub mod tiff;

use std::path::{Path, PathBuf};

use thiserror::Error;

/// A decoded image ready to hand off to the UI layer.
///
/// Pixels are always RGBA8 (4 bytes per pixel, row-major, top-to-bottom).
/// Any format-specific quirks (palette indices, 16-bit depth, HDR float,
/// premultiplied alpha) are normalized to RGBA8 by the codec.
#[derive(Debug, Clone)]
pub struct DecodedImage {
    pub width: u32,
    pub height: u32,
    pub pixels: Vec<u8>,
    /// Original format name, e.g. "PNG", "JPEG", "AVIF".
    pub format: &'static str,
}

/// Why a decode failed.
///
/// The UI layer renders each variant differently:
/// - `AntiListed` → modal overlay explaining the project's stance.
/// - `Unsupported` → "this format isn't supported" message + link to docs.
/// - `Io` / `Decode` → generic error toast.
#[derive(Debug, Clone, Error)]
pub enum DecodeError {
    #[error("File is on the project's anti-list: {format_name}")]
    AntiListed {
        path: PathBuf,
        format_name: &'static str,
        reason: &'static str,
        alternative: &'static str,
    },

    #[error("Unsupported format: {extension}")]
    Unsupported {
        path: PathBuf,
        extension: String,
    },

    #[error("Could not read file: {0}")]
    Io(String),

    #[error("Decoder failed: {0}")]
    Decode(String),
}

impl From<std::io::Error> for DecodeError {
    fn from(e: std::io::Error) -> Self {
        DecodeError::Io(e.to_string())
    }
}

/// Trait every decoder implements.
///
/// Implementations live in submodules (`image_crate`, future `jxl`, `tiff`, etc.).
/// Each codec is registered in [`FormatRegistry::new`] with the extensions it handles.
pub trait Codec: Send + Sync {
    /// Human-readable name, e.g. "PNG", "JPEG XL".
    /// Reserved for diagnostics and a future "supported formats" dialog.
    #[allow(dead_code)]
    fn name(&self) -> &'static str;

    /// Extensions this codec handles, lowercase, no leading dot.
    /// e.g. `["png", "apng"]`.
    fn extensions(&self) -> &'static [&'static str];

    /// Decode the given bytes. The codec does NOT need to verify the format —
    /// the registry already matched by extension before calling this.
    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError>;
}

/// The central format registry. Owns the codec list and the anti-list.
pub struct FormatRegistry {
    codecs: Vec<Box<dyn Codec>>,
}

impl FormatRegistry {
    /// Construct a registry pre-populated with all Tier 1 codecs. Tier 2
    /// codecs (jxl, tiff, svg, exr) will be added here when their modules
    /// land — see `image_crate` for the implementation template.
    pub fn new() -> Self {
        let mut codecs: Vec<Box<dyn Codec>> = Vec::new();

        // Tier 1: all handled by the `image` crate in one codec.
        codecs.push(Box::new(image_crate::ImageCrateCodec));

        // Tier 2 codecs:
        //   - JXL: pure-Rust JPEG XL (jxl-oxide)
        //   - TIFF: direct `tiff` crate for multi-page / 16-bit
        //   - SVG: vector rasterization (usvg + resvg + tiny-skia)
        //   - EXR: pure-Rust OpenEXR (exr) with Reinhard tone-mapping
        codecs.push(Box::new(jxl::JxlCodec));
        codecs.push(Box::new(tiff::TiffCodec));
        codecs.push(Box::new(svg::SvgCodec));
        codecs.push(Box::new(exr::ExrCodec));

        Self { codecs }
    }

    /// Is this file's extension in the anti-list?
    /// Returns the format info if so.
    pub fn check_anti_list(&self, path: &Path) -> Option<anti_list::AntiListedFormat> {
        let ext = path.extension()?.to_str()?.to_ascii_lowercase();
        anti_list::lookup(&ext)
    }

    /// Is this file's extension supported (in any registered codec)?
    pub fn is_supported(&self, path: &Path) -> bool {
        let ext = match path.extension().and_then(|e| e.to_str()) {
            Some(e) => e.to_ascii_lowercase(),
            None => return false,
        };
        self.codecs.iter().any(|c| c.extensions().contains(&ext.as_str()))
    }

    /// Decode a file. Anti-list is checked first; then supported codecs; then
    /// returns `Unsupported` for everything else.
    pub fn decode(&self, path: &Path) -> Result<DecodedImage, DecodeError> {
        // 1. Anti-list check — fail fast, no I/O.
        if let Some(info) = self.check_anti_list(path) {
            return Err(DecodeError::AntiListed {
                path: path.to_path_buf(),
                format_name: info.name,
                reason: info.reason,
                alternative: info.alternative,
            });
        }

        // 2. Find a codec by extension.
        let ext = path
            .extension()
            .and_then(|e| e.to_str())
            .map(|e| e.to_ascii_lowercase())
            .unwrap_or_default();

        let codec = self
            .codecs
            .iter()
            .find(|c| c.extensions().contains(&ext.as_str()));

        let codec = match codec {
            Some(c) => c,
            None => {
                return Err(DecodeError::Unsupported {
                    path: path.to_path_buf(),
                    extension: ext,
                });
            }
        };

        // 3. Read + decode.
        let bytes = std::fs::read(path)?;
        codec.decode(&bytes)
    }

    /// Iterate all supported extensions (used by the folder walker to filter).
    /// Kept public even though the folder walker uses `is_supported` directly —
    /// future callers (e.g. an "Open File" filter dialog) will need it.
    #[allow(dead_code)]
    pub fn supported_extensions(&self) -> Vec<&'static str> {
        self.codecs.iter().flat_map(|c| c.extensions().iter().copied()).collect()
    }
}

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

// ── Pixel rotation ──────────────────────────────────────────────────────

/// Rotate an RGBA pixel buffer by the given degrees (must be 0, 90, 180, or 270).
/// Returns (new_pixels, new_width, new_height).
///
/// 90° = clockwise, 270° = counter-clockwise.
pub fn rotate_rgba(pixels: &[u8], width: u32, height: u32, degrees: i32) -> (Vec<u8>, u32, u32) {
    // Invariant: callers pass one of {0, 90, 180, 270, -90, …}. The modulo
    // normalizes any integer to [0, 360); only the four cardinal values map
    // to a real rotation. Any other value (e.g. 45°) returns the input
    // unchanged — callers in this codebase only ever pass 90-degree steps.
    match ((degrees % 360) + 360) % 360 {
        0 => (pixels.to_vec(), width, height),
        90 => rotate_rgba_cw(pixels, width, height),
        180 => rotate_rgba_180(pixels, width, height),
        270 => rotate_rgba_ccw(pixels, width, height),
        _ => (pixels.to_vec(), width, height),
    }
}

fn rotate_rgba_cw(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 90° clockwise: new[x][y] = old[h-1-y][x]
    // new dimensions: (h, w) — width and height swap
    // Performance: for loops chosen over iterators for pixel-level hot path.
    // SEI CERT/overflow note: the `as usize` casts are safe on 64-bit because
    // pixel count (w*h*4) is bounded by the original allocation, which already
    // fit in usize. On 32-bit, a single image would need to exceed both the
    // address space and the `image` crate's own limits.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = h - 1 - y;
            let ny = x;
            let dst_idx = ((ny * h + nx) * 4) as usize; // new width = old height
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, h, w)
}

fn rotate_rgba_ccw(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 90° counter-clockwise: new[x][y] = old[y][w-1-x]
    // Performance: for loops chosen over iterators for pixel-level hot path.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = y;
            let ny = w - 1 - x;
            let dst_idx = ((ny * h + nx) * 4) as usize; // new width = old height
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, h, w)
}

fn rotate_rgba_180(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 180°: new[x][y] = old[h-1-y][w-1-x]
    // Performance: for loops chosen over iterators for pixel-level hot path.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = w - 1 - x;
            let ny = h - 1 - y;
            let dst_idx = ((ny * w + nx) * 4) as usize;
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, w, h)
}

fn mirror_rgba_horizontal(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // Mirror left↔right (flip X axis).
    // Same dimensions as input.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = w - 1 - x;
            let dst_idx = ((y * w + nx) * 4) as usize;
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, w, h)
}

fn mirror_rgba_vertical(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // Mirror top↔bottom (flip Y axis).
    // Same dimensions as input.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let ny = h - 1 - y;
            let dst_idx = ((ny * w + x) * 4) as usize;
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, w, h)
}

// ── EXIF orientation ───────────────────────────────────────────────────

/// Apply an EXIF orientation tag to an RGBA pixel buffer.
///
/// EXIF orientation values are 1–8 per the EXIF specification:
///
/// ```text
/// 1 = 0°                (no transform — "Normal")
/// 2 = mirror horizontal (flip X)
/// 3 = 180°
/// 4 = mirror vertical   (flip Y)
/// 5 = transpose         (mirror X + 90° CW)
/// 6 = 90° CW            ← most common for phone portrait shots
/// 7 = anti-transpose    (mirror X + 90° CCW)
/// 8 = 90° CCW           ← common for upside-down phone portraits
/// ```
///
/// Phone cameras (iOS, Android) typically save the raw sensor data in
/// landscape orientation and write orientation=6 (or 8) so the viewer
/// rotates it to portrait for display. Without applying this tag, every
/// portrait photo appears sideways.
///
/// Returns `(new_pixels, new_width, new_height)`. For the rotation cases
/// (3, 6, 7, 8) the new dimensions may swap width↔height. For the pure
/// mirror cases (2, 4) and for 1 / unknown values, the dimensions are
/// preserved.
///
/// Unknown values (0, or > 8) are treated as 1 (identity) — the EXIF spec
/// reserves 0 to mean "unknown", and values > 8 are not valid.
pub fn apply_exif_orientation(
    pixels: &[u8],
    width: u32,
    height: u32,
    orientation: u16,
) -> (Vec<u8>, u32, u32) {
    match orientation {
        1 => (pixels.to_vec(), width, height),
        2 => mirror_rgba_horizontal(pixels, width, height),
        3 => rotate_rgba_180(pixels, width, height),
        4 => mirror_rgba_vertical(pixels, width, height),
        5 => {
            // Transpose: reflection across the main diagonal
            // (top-left to bottom-right). Equivalent to: mirror Y then
            // rotate 90° CW. The main diagonal is preserved — pixels on
            // the diagonal stay in place.
            let (m, w, h) = mirror_rgba_vertical(pixels, width, height);
            rotate_rgba_cw(&m, w, h)
        }
        6 => rotate_rgba_cw(pixels, width, height),
        7 => {
            // Transverse: reflection across the anti-diagonal
            // (top-right to bottom-left). Equivalent to: mirror X then
            // rotate 90° CW. The anti-diagonal is preserved.
            let (m, w, h) = mirror_rgba_horizontal(pixels, width, height);
            rotate_rgba_cw(&m, w, h)
        }
        8 => rotate_rgba_ccw(pixels, width, height),
        // 0 (unknown) and any out-of-range value: identity.
        _ => (pixels.to_vec(), width, height),
    }
}

/// Read the EXIF orientation tag from in-memory image bytes.
///
/// Uses `kamadak-exif` to parse the EXIF segment (works for JPEG, TIFF,
/// WebP, HEIF — anything that carries an EXIF container). Returns `None`
/// when the file has no EXIF segment or no orientation tag — callers
/// should treat that as orientation=1 (no transform).
///
/// Reuses the same library the EXIF properties panel uses, but reads from
/// a `Cursor<&[u8]>` rather than re-opening the file from disk — we
/// already have the bytes in memory at decode time, so going back to the
/// filesystem would be wasteful.
pub fn parse_exif_orientation(bytes: &[u8]) -> Option<u16> {
    let cursor = std::io::Cursor::new(bytes);
    let mut bufreader = std::io::BufReader::new(cursor);
    let exif_reader = exif::Reader::new();
    let exif = exif_reader.read_from_container(&mut bufreader).ok()?;

    exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)
        .and_then(|field| match field.value {
            exif::Value::Short(ref shorts) => shorts.first().copied(),
            _ => None,
        })
}


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

    fn make_pixels(w: u32, h: u32) -> Vec<u8> {
        let mut v = Vec::with_capacity((w * h * 4) as usize);
        for y in 0..h {
            for x in 0..w {
                v.push(x as u8);
                v.push(y as u8);
                v.push(0);
                v.push(255);
            }
        }
        v
    }

    #[test]
    fn rotate_0_is_identity() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 0);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        assert_eq!(out, px);
    }

    #[test]
    fn rotate_360_is_identity() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 360);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        assert_eq!(out, px);
    }

    #[test]
    fn rotate_90_swaps_dimensions() {
        let px = make_pixels(3, 2);
        let (_out, w, h) = rotate_rgba(&px, 3, 2, 90);
        assert_eq!(w, 2);
        assert_eq!(h, 3);
    }

    #[test]
    fn rotate_90_four_times_is_identity() {
        let px = make_pixels(5, 3);
        let (p1, w1, h1) = rotate_rgba(&px, 5, 3, 90);
        let (p2, w2, h2) = rotate_rgba(&p1, w1, h1, 90);
        let (p3, w3, h3) = rotate_rgba(&p2, w2, h2, 90);
        let (p4, w4, h4) = rotate_rgba(&p3, w3, h3, 90);
        assert_eq!(w4, 5);
        assert_eq!(h4, 3);
        assert_eq!(p4, px);
    }

    #[test]
    fn rotate_180_keeps_dimensions() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 180);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        // Top-left pixel should become bottom-right
        assert_eq!(out[0..4], px[(3 * 2 - 1) * 4..(3 * 2) * 4]);
    }
}

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

    /// Build a 3×2 RGBA image with a recognizable pattern.
    /// Each pixel is (x, y, 0, 255) so we can identify which source position
    /// ended up at which destination position after the transform.
    fn make_pixels(w: u32, h: u32) -> Vec<u8> {
        let mut v = Vec::with_capacity((w * h * 4) as usize);
        for y in 0..h {
            for x in 0..w {
                v.push(x as u8);
                v.push(y as u8);
                v.push(0);
                v.push(255);
            }
        }
        v
    }

    /// Look up the source position that ended up at destination (x, y).
    /// Each pixel is (R, G, B, A) = (src_x, src_y, 0, 255), so we just read
    /// the first two bytes of the pixel at (x, y).
    fn src_pos_at(out: &[u8], dst_w: u32, x: u32, y: u32) -> (u8, u8) {
        let idx = ((y * dst_w + x) * 4) as usize;
        (out[idx], out[idx + 1])
    }

    #[test]
    fn orientation_1_is_identity() {
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 1);
        assert_eq!((w, h), (3, 2));
        assert_eq!(out, px);
    }

    #[test]
    fn orientation_0_is_treated_as_identity() {
        // EXIF reserves 0 for "unknown" — must not panic, must be identity.
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 0);
        assert_eq!((w, h), (3, 2));
        assert_eq!(out, px);
    }

    #[test]
    fn orientation_3_is_180_rotation() {
        // 180°: top-left of source should land at bottom-right of dst.
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 3);
        assert_eq!((w, h), (3, 2));
        // Dst bottom-right pixel (2, 1) should be src top-left (0, 0).
        assert_eq!(src_pos_at(&out, w, 2, 1), (0, 0));
        // Dst top-left pixel (0, 0) should be src bottom-right (2, 1).
        assert_eq!(src_pos_at(&out, w, 0, 0), (2, 1));
    }

    #[test]
    fn orientation_6_is_90_cw_and_swaps_dimensions() {
        // Orientation 6 is the most common case for phone portrait shots.
        // Source is landscape (3×2), displayed image should be portrait (2×3).
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 6);
        assert_eq!((w, h), (2, 3));
        // For 90° CW: src top-left (0, 0) → dst top-right (w-1=1, 0).
        assert_eq!(src_pos_at(&out, w, 1, 0), (0, 0));
        // src top-right (2, 0) → dst bottom-right (1, 2).
        assert_eq!(src_pos_at(&out, w, 1, 2), (2, 0));
    }

    #[test]
    fn orientation_8_is_90_ccw_and_swaps_dimensions() {
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 8);
        assert_eq!((w, h), (2, 3));
        // For 90° CCW: src top-left (0, 0) → dst bottom-left (0, h-1=2).
        assert_eq!(src_pos_at(&out, w, 0, 2), (0, 0));
    }

    #[test]
    fn orientation_5_preserves_main_diagonal() {
        // Orientation 5 (transpose) reflects across the main diagonal —
        // pixels on the main diagonal stay in place. For a square image,
        // every pixel (x, x) should land at (x, x).
        // Use a 3×3 image so the main diagonal is (0,0)→(1,1)→(2,2).
        let px = make_pixels(3, 3);
        let (out, w, h) = apply_exif_orientation(&px, 3, 3, 5);
        assert_eq!((w, h), (3, 3));
        // Transpose: dst(x, y) = src(y, x). So dst(1, 2) should be src(2, 1).
        assert_eq!(src_pos_at(&out, w, 1, 2), (2, 1));
        // And dst(2, 1) should be src(1, 2).
        assert_eq!(src_pos_at(&out, w, 2, 1), (1, 2));
        // Main diagonal preserved: dst(1, 1) = src(1, 1).
        assert_eq!(src_pos_at(&out, w, 1, 1), (1, 1));
    }

    #[test]
    fn orientation_7_preserves_anti_diagonal() {
        // Orientation 7 (transverse) reflects across the anti-diagonal —
        // pixels on the anti-diagonal stay in place.
        // For a 3×3 image, anti-diagonal is (2,0)→(1,1)→(0,2).
        let px = make_pixels(3, 3);
        let (out, w, h) = apply_exif_orientation(&px, 3, 3, 7);
        assert_eq!((w, h), (3, 3));
        // Anti-diagonal preserved: dst(2, 0) = src(2, 0), dst(0, 2) = src(0, 2).
        assert_eq!(src_pos_at(&out, w, 2, 0), (2, 0));
        assert_eq!(src_pos_at(&out, w, 0, 2), (0, 2));
        // Transverse: dst(x, y) = src(h-1-y, w-1-x). So dst(0, 0) = src(2, 2).
        assert_eq!(src_pos_at(&out, w, 0, 0), (2, 2));
    }

    #[test]
    fn orientation_2_mirrors_horizontally() {
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 2);
        assert_eq!((w, h), (3, 2));
        // Mirror X: dst(x, y) = src(w-1-x, y). dst(0, 0) = src(2, 0).
        assert_eq!(src_pos_at(&out, w, 0, 0), (2, 0));
        assert_eq!(src_pos_at(&out, w, 2, 0), (0, 0));
    }

    #[test]
    fn orientation_4_mirrors_vertically() {
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 4);
        assert_eq!((w, h), (3, 2));
        // Mirror Y: dst(x, y) = src(x, h-1-y). dst(0, 0) = src(0, 1).
        assert_eq!(src_pos_at(&out, w, 0, 0), (0, 1));
        assert_eq!(src_pos_at(&out, w, 0, 1), (0, 0));
    }

    #[test]
    fn all_orientations_round_trip_through_inverse() {
        // For every orientation, applying the same orientation twice should
        // NOT necessarily be identity (mirror X twice is identity, but
        // rotate 90° CW twice is 180°). Instead, verify that applying
        // orientation N to a 1×1 image is always identity (single pixel
        // has no spatial orientation).
        let one_px = vec![7, 8, 9, 255];
        for orient in 0..=10u16 {
            let (out, w, h) = apply_exif_orientation(&one_px, 1, 1, orient);
            assert_eq!((w, h), (1, 1), "orientation {} on 1×1 swapped dims", orient);
            assert_eq!(out, one_px, "orientation {} on 1×1 changed pixels", orient);
        }
    }

    #[test]
    fn orientation_above_8_is_identity() {
        // EXIF spec only defines 1–8; values > 8 are out of range.
        let px = make_pixels(3, 2);
        let (out, w, h) = apply_exif_orientation(&px, 3, 2, 9);
        assert_eq!((w, h), (3, 2));
        assert_eq!(out, px);
    }

    #[test]
    fn parse_exif_orientation_returns_none_for_non_exif_bytes() {
        // A raw PNG without any EXIF chunk — should return None, not panic.
        // Use a minimal PNG header + IHDR (8 + 25 bytes).
        let png_bytes: &[u8] = &[
            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
            0x00, 0x00, 0x00, 0x0D, // IHDR length
            0x49, 0x48, 0x44, 0x52, // "IHDR"
            0x00, 0x00, 0x00, 0x01, // width = 1
            0x00, 0x00, 0x00, 0x01, // height = 1
            0x08, 0x06, 0x00, 0x00, 0x00, // bit depth 8, color type 6 (RGBA)
            0x1f, 0x15, 0xc4, 0x89, // CRC
        ];
        assert!(parse_exif_orientation(png_bytes).is_none());
    }

    #[test]
    fn parse_exif_orientation_returns_none_for_empty_bytes() {
        assert!(parse_exif_orientation(&[]).is_none());
    }

    #[test]
    fn parse_exif_orientation_returns_none_for_random_bytes() {
        let random: &[u8] = &[0xff, 0xfe, 0xfd, 0xfc, 0x00, 0x01, 0x02, 0x03];
        assert!(parse_exif_orientation(random).is_none());
    }
}
