//! Pixel format descriptors and alias resolution.

use crate::{ErrorCategory, ErrorCode, PixelFlowError, Result};

/// Official color range metadata values.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorRange {
    /// Full-range code values.
    Full,
    /// Limited-range code values.
    Limited,
}

impl ColorRange {
    /// Parses canonical range metadata string.
    pub fn parse(value: &str) -> Result<Self> {
        match value {
            "full" => Ok(Self::Full),
            "limited" => Ok(Self::Limited),
            _ => Err(PixelFlowError::new(
                ErrorCategory::Format,
                ErrorCode::new("format.unsupported_range"),
                format!("unsupported color range '{value}'"),
            )),
        }
    }

    /// Returns canonical metadata string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Full => "full",
            Self::Limited => "limited",
        }
    }
}

/// Official matrix coefficient metadata values.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorMatrix {
    /// Identity matrix coefficients.
    Identity,
    /// ITU-R BT.709 matrix coefficients.
    Bt709,
    /// ITU-R BT.470 System M matrix coefficients.
    Bt470M,
    /// ITU-R BT.470 System B/G matrix coefficients.
    Bt470Bg,
    /// SMPTE 170M matrix coefficients.
    Smpte170M,
    /// SMPTE 240M matrix coefficients.
    Smpte240M,
    /// YCgCo matrix coefficients.
    Ycgco,
    /// ITU-R BT.2020 non-constant-luminance matrix coefficients.
    Bt2020Ncl,
    /// ITU-R BT.2020 constant-luminance matrix coefficients.
    Bt2020Cl,
    /// SMPTE ST 2085 matrix coefficients.
    Smpte2085,
    /// Chromaticity-derived non-constant-luminance matrix coefficients.
    ChromaticityDerivedNcl,
    /// Chromaticity-derived constant-luminance matrix coefficients.
    ChromaticityDerivedCl,
    /// ICtCp matrix coefficients.
    Ictcp,
}

impl ColorMatrix {
    /// Parses canonical matrix metadata string.
    pub fn parse(value: &str) -> Result<Self> {
        match value {
            "identity" => Ok(Self::Identity),
            "bt709" => Ok(Self::Bt709),
            "bt470m" => Ok(Self::Bt470M),
            "bt470bg" => Ok(Self::Bt470Bg),
            "smpte170m" => Ok(Self::Smpte170M),
            "smpte240m" => Ok(Self::Smpte240M),
            "ycgco" => Ok(Self::Ycgco),
            "bt2020_ncl" => Ok(Self::Bt2020Ncl),
            "bt2020_cl" => Ok(Self::Bt2020Cl),
            "smpte2085" => Ok(Self::Smpte2085),
            "chromaticity_derived_ncl" => Ok(Self::ChromaticityDerivedNcl),
            "chromaticity_derived_cl" => Ok(Self::ChromaticityDerivedCl),
            "ictcp" => Ok(Self::Ictcp),
            _ => Err(color_metadata_error(
                "format.unsupported_matrix",
                format!("unsupported color matrix '{value}'"),
            )),
        }
    }

    /// Returns canonical metadata string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Identity => "identity",
            Self::Bt709 => "bt709",
            Self::Bt470M => "bt470m",
            Self::Bt470Bg => "bt470bg",
            Self::Smpte170M => "smpte170m",
            Self::Smpte240M => "smpte240m",
            Self::Ycgco => "ycgco",
            Self::Bt2020Ncl => "bt2020_ncl",
            Self::Bt2020Cl => "bt2020_cl",
            Self::Smpte2085 => "smpte2085",
            Self::ChromaticityDerivedNcl => "chromaticity_derived_ncl",
            Self::ChromaticityDerivedCl => "chromaticity_derived_cl",
            Self::Ictcp => "ictcp",
        }
    }

    /// Returns `(Kr, Kg, Kb)` luma coefficients for matrix families.
    #[must_use]
    pub const fn luma_coefficients(self) -> (f32, f32, f32) {
        match self {
            Self::Identity | Self::Ycgco | Self::Ictcp => (1.0, 1.0, 1.0),
            Self::Bt709 => (0.2126, 0.7152, 0.0722),
            Self::Bt470M => (0.30, 0.59, 0.11),
            Self::Bt470Bg | Self::Smpte170M => (0.299, 0.587, 0.114),
            Self::Smpte240M => (0.212, 0.701, 0.087),
            Self::Bt2020Ncl | Self::Bt2020Cl | Self::Smpte2085 => (0.2627, 0.6780, 0.0593),
            Self::ChromaticityDerivedNcl | Self::ChromaticityDerivedCl => (0.0, 0.0, 0.0),
        }
    }
}

/// Official transfer-function metadata values.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorTransfer {
    /// ITU-R BT.1886 transfer function.
    Bt1886,
    /// ITU-R BT.470 System M transfer function.
    Bt470M,
    /// ITU-R BT.470 System B/G transfer function.
    Bt470Bg,
    /// SMPTE 170M transfer function.
    Smpte170M,
    /// SMPTE 240M transfer function.
    Smpte240M,
    /// Linear-light transfer function.
    Linear,
    /// Logarithmic curve with 100:1 range.
    Log100,
    /// Logarithmic curve with 316:1 range.
    Log316,
    /// xvYCC transfer function.
    Xvycc,
    /// ITU-R BT.1361 extended-color-gamut transfer function.
    Bt1361E,
    /// IEC sRGB transfer function.
    Srgb,
    /// ITU-R BT.2020 10-bit transfer function.
    Bt2020_10,
    /// ITU-R BT.2020 12-bit transfer function.
    Bt2020_12,
    /// SMPTE ST 2084 perceptual quantizer.
    Pq,
    /// SMPTE ST 428 transfer function.
    Smpte428,
    /// ARIB STD-B67 hybrid log-gamma.
    Hlg,
}

impl ColorTransfer {
    /// Parses canonical transfer metadata string.
    pub fn parse(value: &str) -> Result<Self> {
        match value {
            "bt1886" => Ok(Self::Bt1886),
            "bt470m" => Ok(Self::Bt470M),
            "bt470bg" => Ok(Self::Bt470Bg),
            "smpte170m" => Ok(Self::Smpte170M),
            "smpte240m" => Ok(Self::Smpte240M),
            "linear" => Ok(Self::Linear),
            "log100" => Ok(Self::Log100),
            "log316" => Ok(Self::Log316),
            "xvycc" => Ok(Self::Xvycc),
            "bt1361e" => Ok(Self::Bt1361E),
            "srgb" => Ok(Self::Srgb),
            "bt2020_10" => Ok(Self::Bt2020_10),
            "bt2020_12" => Ok(Self::Bt2020_12),
            "pq" => Ok(Self::Pq),
            "smpte428" => Ok(Self::Smpte428),
            "hlg" => Ok(Self::Hlg),
            _ => Err(color_metadata_error(
                "format.unsupported_transfer",
                format!("unsupported color transfer '{value}'"),
            )),
        }
    }

    /// Returns canonical metadata string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Bt1886 => "bt1886",
            Self::Bt470M => "bt470m",
            Self::Bt470Bg => "bt470bg",
            Self::Smpte170M => "smpte170m",
            Self::Smpte240M => "smpte240m",
            Self::Linear => "linear",
            Self::Log100 => "log100",
            Self::Log316 => "log316",
            Self::Xvycc => "xvycc",
            Self::Bt1361E => "bt1361e",
            Self::Srgb => "srgb",
            Self::Bt2020_10 => "bt2020_10",
            Self::Bt2020_12 => "bt2020_12",
            Self::Pq => "pq",
            Self::Smpte428 => "smpte428",
            Self::Hlg => "hlg",
        }
    }
}

/// Official color-primaries metadata values.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorPrimaries {
    /// ITU-R BT.709 primaries.
    Bt709,
    /// ITU-R BT.470 System M primaries.
    Bt470M,
    /// ITU-R BT.470 System B/G primaries.
    Bt470Bg,
    /// SMPTE 170M primaries.
    Smpte170M,
    /// SMPTE 240M primaries.
    Smpte240M,
    /// Film primaries.
    Film,
    /// ITU-R BT.2020 primaries.
    Bt2020,
    /// SMPTE ST 428 primaries.
    Smpte428,
    /// DCI-P3 primaries.
    DciP3,
    /// Display P3 primaries.
    DisplayP3,
}

impl ColorPrimaries {
    /// Parses canonical primaries metadata string.
    pub fn parse(value: &str) -> Result<Self> {
        match value {
            "bt709" => Ok(Self::Bt709),
            "bt470m" => Ok(Self::Bt470M),
            "bt470bg" => Ok(Self::Bt470Bg),
            "smpte170m" => Ok(Self::Smpte170M),
            "smpte240m" => Ok(Self::Smpte240M),
            "film" => Ok(Self::Film),
            "bt2020" => Ok(Self::Bt2020),
            "smpte428" => Ok(Self::Smpte428),
            "dci_p3" => Ok(Self::DciP3),
            "display_p3" => Ok(Self::DisplayP3),
            _ => Err(color_metadata_error(
                "format.unsupported_primaries",
                format!("unsupported color primaries '{value}'"),
            )),
        }
    }

    /// Returns canonical metadata string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Bt709 => "bt709",
            Self::Bt470M => "bt470m",
            Self::Bt470Bg => "bt470bg",
            Self::Smpte170M => "smpte170m",
            Self::Smpte240M => "smpte240m",
            Self::Film => "film",
            Self::Bt2020 => "bt2020",
            Self::Smpte428 => "smpte428",
            Self::DciP3 => "dci_p3",
            Self::DisplayP3 => "display_p3",
        }
    }
}

/// Official chroma-siting metadata values.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChromaSiting {
    /// Chroma samples sit on left edge and centered vertically.
    Left,
    /// Chroma samples are centered horizontally and vertically.
    Center,
    /// Chroma samples sit on top-left corner.
    TopLeft,
    /// Chroma samples are centered horizontally on top edge.
    Top,
    /// Chroma samples sit on bottom-left corner.
    BottomLeft,
    /// Chroma samples are centered horizontally on bottom edge.
    Bottom,
}

impl ChromaSiting {
    /// Parses canonical chroma-siting metadata string.
    pub fn parse(value: &str) -> Result<Self> {
        match value {
            "left" => Ok(Self::Left),
            "center" => Ok(Self::Center),
            "top_left" => Ok(Self::TopLeft),
            "top" => Ok(Self::Top),
            "bottom_left" => Ok(Self::BottomLeft),
            "bottom" => Ok(Self::Bottom),
            _ => Err(color_metadata_error(
                "format.unsupported_chroma_siting",
                format!("unsupported chroma siting '{value}'"),
            )),
        }
    }

    /// Returns canonical metadata string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Left => "left",
            Self::Center => "center",
            Self::TopLeft => "top_left",
            Self::Top => "top",
            Self::BottomLeft => "bottom_left",
            Self::Bottom => "bottom",
        }
    }

    /// Returns `(x, y)` chroma offsets in luma-space coordinates.
    #[must_use]
    pub const fn offsets(self) -> (f32, f32) {
        match self {
            Self::Left => (0.0, 0.5),
            Self::Center => (0.5, 0.5),
            Self::TopLeft => (0.0, 0.0),
            Self::Top => (0.5, 0.0),
            Self::BottomLeft => (0.0, 1.0),
            Self::Bottom => (0.5, 1.0),
        }
    }
}

fn color_metadata_error(code: &'static str, message: impl Into<String>) -> PixelFlowError {
    PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
}

/// Stored scalar type for plane samples.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SampleType {
    /// Unsigned 8-bit integer sample.
    U8,
    /// Unsigned 16-bit integer sample. Logical bit depths 9 through 16 are stored here.
    U16,
    /// 32-bit float sample.
    F32,
}

impl SampleType {
    /// Returns byte size of one stored sample.
    #[must_use]
    pub const fn bytes_per_sample(self) -> usize {
        match self {
            Self::U8 => 1,
            Self::U16 => 2,
            Self::F32 => 4,
        }
    }
}

/// High-level pixel family.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FormatFamily {
    /// Single-plane luma format.
    Gray,
    /// Three-plane YUV format.
    Yuv,
    /// Three-plane planar RGB format.
    PlanarRgb,
}

/// Chroma subsampling for YUV formats.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChromaSubsampling {
    /// 4:4:4 with full chroma resolution.
    Cs444,
    /// 4:2:2 with half horizontal chroma.
    Cs422,
    /// 4:2:0 with half horizontal and vertical chroma.
    Cs420,
}

/// Semantic role of one plane.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlaneRole {
    /// Gray plane.
    Gray,
    /// Y luma plane.
    Y,
    /// U chroma plane.
    U,
    /// V chroma plane.
    V,
    /// R plane.
    R,
    /// G plane.
    G,
    /// B plane.
    B,
}

/// Descriptor for one plane in a format.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlaneDescriptor {
    /// Plane role.
    pub role: PlaneRole,
    /// Divisor from full frame width for this plane.
    pub width_divisor: usize,
    /// Divisor from full frame height for this plane.
    pub height_divisor: usize,
    /// Stored sample type.
    pub sample_type: SampleType,
}

/// Stable descriptor for a concrete planar format.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FormatDescriptor {
    name: String,
    family: FormatFamily,
    subsampling: Option<ChromaSubsampling>,
    bits_per_sample: u8,
    sample_type: SampleType,
    planes: Vec<PlaneDescriptor>,
}

impl FormatDescriptor {
    /// Returns canonical format name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns format family.
    #[must_use]
    pub const fn family(&self) -> FormatFamily {
        self.family
    }

    /// Returns chroma subsampling for YUV formats.
    #[must_use]
    pub const fn subsampling(&self) -> Option<ChromaSubsampling> {
        self.subsampling
    }

    /// Returns logical bit depth.
    #[must_use]
    pub const fn bits_per_sample(&self) -> u8 {
        self.bits_per_sample
    }

    /// Returns stored sample type.
    #[must_use]
    pub const fn sample_type(&self) -> SampleType {
        self.sample_type
    }

    /// Returns plane descriptors in storage order.
    #[must_use]
    pub fn planes(&self) -> &[PlaneDescriptor] {
        &self.planes
    }
}

/// Derives same-family format at different logical bit depth.
pub fn format_with_bit_depth(format: &FormatDescriptor, bits: u8) -> Result<FormatDescriptor> {
    let suffix = match bits {
        8 => "8",
        10 => "10",
        12 => "12",
        16 => "16",
        32 => "f32",
        _ => {
            return Err(PixelFlowError::new(
                ErrorCategory::Format,
                ErrorCode::new("format.unsupported_bit_depth"),
                format!("unsupported bit depth '{bits}'"),
            ));
        }
    };

    let alias = match (format.family(), format.subsampling()) {
        (FormatFamily::Gray, None) => format!("gray{suffix}"),
        (FormatFamily::Yuv, Some(ChromaSubsampling::Cs420)) => format!("yuv420p{suffix}"),
        (FormatFamily::Yuv, Some(ChromaSubsampling::Cs422)) => format!("yuv422p{suffix}"),
        (FormatFamily::Yuv, Some(ChromaSubsampling::Cs444)) => format!("yuv444p{suffix}"),
        (FormatFamily::PlanarRgb, None) => format!("rgbp{suffix}"),
        _ => {
            return Err(PixelFlowError::new(
                ErrorCategory::Format,
                ErrorCode::new("format.unsupported_bit_depth"),
                format!("cannot derive bit depth format from '{}'", format.name()),
            ));
        }
    };

    resolve_format_alias(&alias)
}

/// Resolves a user-facing alias to stable format descriptor.
pub fn resolve_format_alias(alias: &str) -> Result<FormatDescriptor> {
    let normalized = alias.to_ascii_lowercase();

    if let Some(descriptor) = resolve_gray(&normalized) {
        return Ok(descriptor);
    }
    if let Some(descriptor) = resolve_yuv(&normalized) {
        return Ok(descriptor);
    }
    if let Some(descriptor) = resolve_planar_rgb(&normalized) {
        return Ok(descriptor);
    }

    Err(PixelFlowError::new(
        ErrorCategory::Format,
        ErrorCode::new("format.unsupported_alias"),
        format!("unsupported format alias '{alias}'"),
    ))
}

fn resolve_gray(alias: &str) -> Option<FormatDescriptor> {
    let suffix = alias.strip_prefix("gray")?;
    let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
    Some(FormatDescriptor {
        name: format!("gray{canonical_suffix}"),
        family: FormatFamily::Gray,
        subsampling: None,
        bits_per_sample: bits,
        sample_type: sample,
        planes: vec![PlaneDescriptor {
            role: PlaneRole::Gray,
            width_divisor: 1,
            height_divisor: 1,
            sample_type: sample,
        }],
    })
}

fn resolve_yuv(alias: &str) -> Option<FormatDescriptor> {
    let (prefix, subsampling) = [
        ("yuv420p", ChromaSubsampling::Cs420),
        ("yuv422p", ChromaSubsampling::Cs422),
        ("yuv444p", ChromaSubsampling::Cs444),
    ]
    .into_iter()
    .find(|(prefix, _)| alias.starts_with(prefix))?;

    let suffix = alias.strip_prefix(prefix)?;
    let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
    let (chroma_w_div, chroma_h_div) = match subsampling {
        ChromaSubsampling::Cs444 => (1, 1),
        ChromaSubsampling::Cs422 => (2, 1),
        ChromaSubsampling::Cs420 => (2, 2),
    };

    Some(FormatDescriptor {
        name: format!("{prefix}{canonical_suffix}"),
        family: FormatFamily::Yuv,
        subsampling: Some(subsampling),
        bits_per_sample: bits,
        sample_type: sample,
        planes: vec![
            PlaneDescriptor {
                role: PlaneRole::Y,
                width_divisor: 1,
                height_divisor: 1,
                sample_type: sample,
            },
            PlaneDescriptor {
                role: PlaneRole::U,
                width_divisor: chroma_w_div,
                height_divisor: chroma_h_div,
                sample_type: sample,
            },
            PlaneDescriptor {
                role: PlaneRole::V,
                width_divisor: chroma_w_div,
                height_divisor: chroma_h_div,
                sample_type: sample,
            },
        ],
    })
}

fn resolve_planar_rgb(alias: &str) -> Option<FormatDescriptor> {
    let suffix = alias
        .strip_prefix("rgbp")
        .or_else(|| alias.strip_prefix("gbrp"))?;
    let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;

    Some(FormatDescriptor {
        name: format!("rgbp{canonical_suffix}"),
        family: FormatFamily::PlanarRgb,
        subsampling: None,
        bits_per_sample: bits,
        sample_type: sample,
        planes: vec![
            PlaneDescriptor {
                role: PlaneRole::R,
                width_divisor: 1,
                height_divisor: 1,
                sample_type: sample,
            },
            PlaneDescriptor {
                role: PlaneRole::G,
                width_divisor: 1,
                height_divisor: 1,
                sample_type: sample,
            },
            PlaneDescriptor {
                role: PlaneRole::B,
                width_divisor: 1,
                height_divisor: 1,
                sample_type: sample,
            },
        ],
    })
}

fn parse_sample_suffix(suffix: &str) -> Option<(u8, SampleType, &'static str)> {
    match suffix {
        "" | "8" => Some((8, SampleType::U8, "8")),
        "10" => Some((10, SampleType::U16, "10")),
        "12" => Some((12, SampleType::U16, "12")),
        "16" => Some((16, SampleType::U16, "16")),
        "f32" => Some((32, SampleType::F32, "f32")),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    #![expect(clippy::indexing_slicing, reason = "allow in tests")]

    use crate::{ErrorCategory, ErrorCode};

    use super::{
        ChromaSiting, ChromaSubsampling, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer,
        FormatFamily, PlaneRole, SampleType, format_with_bit_depth, resolve_format_alias,
    };

    #[test]
    fn color_metadata_parse_accepts_canonical_names() {
        assert_eq!(
            ColorRange::parse("full").expect("full range"),
            ColorRange::Full
        );
        assert_eq!(
            ColorMatrix::parse("identity").expect("identity matrix"),
            ColorMatrix::Identity
        );
        assert_eq!(
            ColorMatrix::parse("bt709").expect("bt709 matrix"),
            ColorMatrix::Bt709
        );
        assert_eq!(
            ColorMatrix::parse("bt470m").expect("bt470m matrix"),
            ColorMatrix::Bt470M
        );
        assert_eq!(
            ColorMatrix::parse("bt470bg").expect("bt470bg matrix"),
            ColorMatrix::Bt470Bg
        );
        assert_eq!(
            ColorMatrix::parse("smpte170m").expect("smpte170m matrix"),
            ColorMatrix::Smpte170M
        );
        assert_eq!(
            ColorMatrix::parse("smpte240m").expect("smpte240m matrix"),
            ColorMatrix::Smpte240M
        );
        assert_eq!(
            ColorMatrix::parse("ycgco").expect("ycgco matrix"),
            ColorMatrix::Ycgco
        );
        assert_eq!(
            ColorMatrix::parse("bt2020_ncl").expect("bt2020 matrix"),
            ColorMatrix::Bt2020Ncl
        );
        assert_eq!(
            ColorMatrix::parse("bt2020_cl").expect("bt2020 CL matrix"),
            ColorMatrix::Bt2020Cl
        );
        assert_eq!(
            ColorMatrix::parse("smpte2085").expect("smpte2085 matrix"),
            ColorMatrix::Smpte2085
        );
        assert_eq!(
            ColorMatrix::parse("chromaticity_derived_ncl").expect("chromaticity NCL matrix"),
            ColorMatrix::ChromaticityDerivedNcl
        );
        assert_eq!(
            ColorMatrix::parse("chromaticity_derived_cl").expect("chromaticity CL matrix"),
            ColorMatrix::ChromaticityDerivedCl
        );
        assert_eq!(
            ColorMatrix::parse("ictcp").expect("ICtCp matrix"),
            ColorMatrix::Ictcp
        );

        assert_eq!(
            ColorTransfer::parse("bt1886").expect("BT.1886 transfer"),
            ColorTransfer::Bt1886
        );
        assert_eq!(
            ColorTransfer::parse("bt470m").expect("BT.470M transfer"),
            ColorTransfer::Bt470M
        );
        assert_eq!(
            ColorTransfer::parse("bt470bg").expect("BT.470BG transfer"),
            ColorTransfer::Bt470Bg
        );
        assert_eq!(
            ColorTransfer::parse("smpte170m").expect("SMPTE 170M transfer"),
            ColorTransfer::Smpte170M
        );
        assert_eq!(
            ColorTransfer::parse("smpte240m").expect("SMPTE 240M transfer"),
            ColorTransfer::Smpte240M
        );
        assert_eq!(
            ColorTransfer::parse("linear").expect("linear transfer"),
            ColorTransfer::Linear
        );
        assert_eq!(
            ColorTransfer::parse("log100").expect("Log100 transfer"),
            ColorTransfer::Log100
        );
        assert_eq!(
            ColorTransfer::parse("log316").expect("Log316 transfer"),
            ColorTransfer::Log316
        );
        assert_eq!(
            ColorTransfer::parse("xvycc").expect("xvYCC transfer"),
            ColorTransfer::Xvycc
        );
        assert_eq!(
            ColorTransfer::parse("bt1361e").expect("BT.1361E transfer"),
            ColorTransfer::Bt1361E
        );
        assert_eq!(
            ColorTransfer::parse("srgb").expect("sRGB transfer"),
            ColorTransfer::Srgb
        );
        assert_eq!(
            ColorTransfer::parse("bt2020_10").expect("bt2020 transfer"),
            ColorTransfer::Bt2020_10
        );
        assert_eq!(
            ColorTransfer::parse("bt2020_12").expect("bt2020 transfer"),
            ColorTransfer::Bt2020_12
        );
        assert_eq!(
            ColorTransfer::parse("pq").expect("PQ transfer"),
            ColorTransfer::Pq
        );
        assert_eq!(
            ColorTransfer::parse("smpte428").expect("SMPTE 428 transfer"),
            ColorTransfer::Smpte428
        );
        assert_eq!(
            ColorTransfer::parse("hlg").expect("HLG transfer"),
            ColorTransfer::Hlg
        );

        assert_eq!(
            ColorPrimaries::parse("bt709").expect("BT.709 primaries"),
            ColorPrimaries::Bt709
        );
        assert_eq!(
            ColorPrimaries::parse("bt470m").expect("BT.470M primaries"),
            ColorPrimaries::Bt470M
        );
        assert_eq!(
            ColorPrimaries::parse("bt470bg").expect("BT.470BG primaries"),
            ColorPrimaries::Bt470Bg
        );
        assert_eq!(
            ColorPrimaries::parse("smpte170m").expect("SMPTE 170M primaries"),
            ColorPrimaries::Smpte170M
        );
        assert_eq!(
            ColorPrimaries::parse("smpte240m").expect("SMPTE 240M primaries"),
            ColorPrimaries::Smpte240M
        );
        assert_eq!(
            ColorPrimaries::parse("film").expect("film primaries"),
            ColorPrimaries::Film
        );
        assert_eq!(
            ColorPrimaries::parse("bt2020").expect("BT.2020 primaries"),
            ColorPrimaries::Bt2020
        );
        assert_eq!(
            ColorPrimaries::parse("smpte428").expect("SMPTE 428 primaries"),
            ColorPrimaries::Smpte428
        );
        assert_eq!(
            ColorPrimaries::parse("dci_p3").expect("DCI-P3 primaries"),
            ColorPrimaries::DciP3
        );
        assert_eq!(
            ColorPrimaries::parse("display_p3").expect("Display P3 primaries"),
            ColorPrimaries::DisplayP3
        );
        assert_eq!(
            ChromaSiting::parse("top_left").expect("top-left siting"),
            ChromaSiting::TopLeft
        );

        assert_eq!(ColorMatrix::Ictcp.as_str(), "ictcp");
        assert_eq!(ColorTransfer::Pq.as_str(), "pq");
        assert_eq!(ColorPrimaries::DisplayP3.as_str(), "display_p3");
        assert_eq!(ChromaSiting::Bottom.offsets(), (0.5, 1.0));
    }

    #[test]
    fn color_metadata_parse_rejects_noncanonical_names() {
        for (label, error) in [
            (
                "matrix",
                ColorMatrix::parse("rec709").expect_err("alias should fail"),
            ),
            (
                "transfer",
                ColorTransfer::parse("bt.1886").expect_err("punctuated name should fail"),
            ),
            (
                "primaries",
                ColorPrimaries::parse("p3").expect_err("alias should fail"),
            ),
            (
                "siting",
                ChromaSiting::parse("mpeg2").expect_err("alias should fail"),
            ),
        ] {
            assert_eq!(error.category(), ErrorCategory::Format, "{label}");
        }
    }

    #[test]
    fn color_metadata_matrix_luma_coefficients_are_stable() {
        assert_eq!(
            ColorMatrix::Bt709.luma_coefficients(),
            (0.2126, 0.7152, 0.0722)
        );
        assert_eq!(ColorMatrix::Bt470M.luma_coefficients(), (0.30, 0.59, 0.11));
        assert_eq!(
            ColorMatrix::Bt470Bg.luma_coefficients(),
            (0.299, 0.587, 0.114)
        );
        assert_eq!(
            ColorMatrix::Smpte170M.luma_coefficients(),
            (0.299, 0.587, 0.114)
        );
        assert_eq!(
            ColorMatrix::Smpte240M.luma_coefficients(),
            (0.212, 0.701, 0.087)
        );
        assert_eq!(
            ColorMatrix::Bt2020Ncl.luma_coefficients(),
            (0.2627, 0.6780, 0.0593)
        );
    }

    #[test]
    fn color_range_parse_accepts_official_names() {
        assert_eq!(
            ColorRange::parse("full").expect("full should parse"),
            ColorRange::Full
        );
        assert_eq!(
            ColorRange::parse("limited").expect("limited should parse"),
            ColorRange::Limited
        );
        assert_eq!(ColorRange::Full.as_str(), "full");
        assert_eq!(ColorRange::Limited.as_str(), "limited");
    }

    #[test]
    fn color_range_parse_rejects_unknown_names() {
        let error = ColorRange::parse("tv").expect_err("unknown range should fail");

        assert_eq!(error.category(), ErrorCategory::Format);
        assert_eq!(error.code(), ErrorCode::new("format.unsupported_range"));
    }

    #[test]
    fn format_with_bit_depth_preserves_family_and_subsampling() {
        let yuv = resolve_format_alias("yuv420p8").expect("alias should resolve");
        let rgb = resolve_format_alias("rgbp16").expect("alias should resolve");
        let gray = resolve_format_alias("grayf32").expect("alias should resolve");

        assert_eq!(
            format_with_bit_depth(&yuv, 10)
                .expect("10-bit YUV should derive")
                .name(),
            "yuv420p10"
        );
        assert_eq!(
            format_with_bit_depth(&rgb, 32)
                .expect("f32 RGB should derive")
                .name(),
            "rgbpf32"
        );
        assert_eq!(
            format_with_bit_depth(&gray, 8)
                .expect("8-bit gray should derive")
                .name(),
            "gray8"
        );
    }

    #[test]
    fn format_with_bit_depth_rejects_unsupported_depth() {
        let format = resolve_format_alias("gray8").expect("alias should resolve");
        let error = format_with_bit_depth(&format, 14).expect_err("14-bit output should fail");

        assert_eq!(error.category(), ErrorCategory::Format);
        assert_eq!(error.code(), ErrorCode::new("format.unsupported_bit_depth"));
    }

    #[test]
    fn resolves_yuv420p10_to_stable_descriptor() {
        let descriptor = resolve_format_alias("yuv420p10").expect("alias should resolve");

        assert_eq!(descriptor.name(), "yuv420p10");
        assert_eq!(descriptor.family(), FormatFamily::Yuv);
        assert_eq!(descriptor.subsampling(), Some(ChromaSubsampling::Cs420));
        assert_eq!(descriptor.bits_per_sample(), 10);
        assert_eq!(descriptor.sample_type(), SampleType::U16);
        assert_eq!(descriptor.planes().len(), 3);
        assert_eq!(descriptor.planes()[0].role, PlaneRole::Y);
        assert_eq!(descriptor.planes()[1].role, PlaneRole::U);
        assert_eq!(descriptor.planes()[1].width_divisor, 2);
        assert_eq!(descriptor.planes()[1].height_divisor, 2);
    }

    #[test]
    fn resolves_planar_rgb_aliases_to_same_descriptor() {
        let rgb = resolve_format_alias("rgbp10").expect("alias should resolve");
        let gbr = resolve_format_alias("gbrp10").expect("alias should resolve");

        assert_eq!(rgb, gbr);
        assert_eq!(rgb.family(), FormatFamily::PlanarRgb);
        assert_eq!(rgb.planes()[0].role, PlaneRole::R);
        assert_eq!(rgb.planes()[1].role, PlaneRole::G);
        assert_eq!(rgb.planes()[2].role, PlaneRole::B);
    }

    #[test]
    fn resolves_float_gray_descriptor() {
        let descriptor = resolve_format_alias("grayf32").expect("alias should resolve");

        assert_eq!(descriptor.name(), "grayf32");
        assert_eq!(descriptor.family(), FormatFamily::Gray);
        assert_eq!(descriptor.bits_per_sample(), 32);
        assert_eq!(descriptor.sample_type(), SampleType::F32);
        assert_eq!(descriptor.planes()[0].role, PlaneRole::Gray);
    }

    #[test]
    fn unsupported_alias_returns_structured_format_error() {
        let error = resolve_format_alias("yuv420p14").expect_err("unsupported alias should fail");

        assert_eq!(error.category(), ErrorCategory::Format);
        assert_eq!(error.code(), ErrorCode::new("format.unsupported_alias"));
        assert!(error.message().contains("yuv420p14"));
    }
}
