#[cfg(any(feature = "alloc", feature = "std"))]
mod audio;
#[cfg(any(feature = "alloc", feature = "std"))]
mod extended_blocks;
#[cfg(any(feature = "alloc", feature = "std"))]
mod hdmi_vsdb;

#[cfg(any(feature = "alloc", feature = "std"))]
pub use audio::{AudioFormat, AudioFormatInfo, AudioSampleRates, ShortAudioDescriptor};
pub use display_types::cea861::Cea861Flags;
#[cfg(any(feature = "alloc", feature = "std"))]
pub use display_types::cea861::{Cea861Capabilities, HdmiAudioBlock};
#[cfg(any(feature = "alloc", feature = "std"))]
pub use extended_blocks::{
    ColorimetryBlock, ColorimetryFlags, DtcPointEncoding, HdmiDscMaxSlices, HdmiForumDsc,
    HdmiForumFrl, HdmiForumSinkCap, HdrDynamicMetadataDescriptor, HdrEotf, HdrStaticMetadata,
    InfoFrameDescriptor, RoomConfigurationBlock, SpeakerAllocation, SpeakerAllocationFlags,
    SpeakerAllocationFlags2, SpeakerAllocationFlags3, SpeakerLocationEntry, T7VtdbBlock,
    T8VtdbBlock, T10VtdbBlock, T10VtdbEntry, VendorSpecificBlock, VesaDisplayDeviceBlock,
    VesaTransferCharacteristic, VideoCapability, VideoCapabilityFlags, VtbExtBlock, infoframe_type,
};
#[cfg(any(feature = "alloc", feature = "std"))]
pub use hdmi_vsdb::{HdmiVsdb, HdmiVsdbFlags};

pub(crate) use display_types::cea861::{dmt_to_mode, vic_to_mode};

#[cfg(any(feature = "alloc", feature = "std"))]
use crate::capabilities::base::timings::decode_dtd_slot;
use crate::capabilities::base::timings::decode_dtd_slot_into_sink;
use crate::model::capabilities::DisplayCapabilities;
use crate::model::capabilities::ModeSink;
use crate::model::capabilities::StaticContext;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::capabilities::VideoMode;
use crate::model::diagnostics::EdidWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::diagnostics::ParseWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::extension::ExtensionHandler;
use crate::model::extension::StaticExtensionHandler;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::prelude::{Arc, Vec};
#[cfg(any(feature = "alloc", feature = "std"))]
use audio::parse_audio_data_block;
#[cfg(any(feature = "alloc", feature = "std"))]
use extended_blocks::{
    EXT_TAG_COLORIMETRY, EXT_TAG_HDMI_AUDIO, EXT_TAG_HDR_DYNAMIC_METADATA,
    EXT_TAG_HDR_STATIC_METADATA, EXT_TAG_HF_EEODB, EXT_TAG_HF_SCDB, EXT_TAG_INFOFRAME,
    EXT_TAG_ROOM_CONFIGURATION, EXT_TAG_SPEAKER_LOCATION, EXT_TAG_T7VTDB, EXT_TAG_T8VTDB,
    EXT_TAG_T10VTDB, EXT_TAG_VESA_DDDB, EXT_TAG_VIDEO_CAPABILITY, EXT_TAG_VIDEO_FORMAT_PREFERENCE,
    EXT_TAG_VSADB, EXT_TAG_VSVDB, EXT_TAG_VTB_EXT, EXT_TAG_Y420_CAPABILITY_MAP, EXT_TAG_Y420_VIDEO,
    parse_colorimetry, parse_hdr_dynamic_metadata, parse_hdr_static_metadata, parse_hf_eeodb,
    parse_hf_scdb, parse_hf_vsdb, parse_infoframe_db, parse_room_configuration,
    parse_speaker_allocation, parse_speaker_location, parse_t7vtdb, parse_t8vtdb, parse_t10vtdb,
    parse_vendor_specific_block, parse_vesa_display_device, parse_vesa_transfer_characteristic,
    parse_video_capability, parse_video_format_preferences, parse_vtb_ext,
    parse_y420_capability_map, parse_y420_vdb,
};
#[cfg(any(feature = "alloc", feature = "std"))]
use hdmi_vsdb::parse_hdmi_vsdb;
/// Processes a CEA-861 extension block (tag `0x02`).
#[derive(Debug)]
pub struct Cea861Handler;

#[cfg(any(feature = "alloc", feature = "std"))]
impl ExtensionHandler for Cea861Handler {
    fn process(
        &self,
        blocks: &[&[u8; 128]],
        caps: &mut DisplayCapabilities,
        warnings: &mut Vec<ParseWarning>,
    ) {
        let Some(first) = blocks.first() else { return };
        let flags = Cea861Flags::from_bits_truncate(first[3]);

        let mut cea_caps = Cea861Capabilities::new(flags);

        for ext in blocks {
            // Parse the data block collection: bytes 4 through dtd_offset-1.
            // When dtd_offset == 0 the spec says no DTDs are present, so data blocks
            // may fill the rest of the block (bytes 4–127).
            let dtd_offset = ext[2] as usize;
            let collection_end = if dtd_offset == 0 {
                128
            } else {
                dtd_offset.min(128)
            };

            if collection_end > 4 {
                let collection = &ext[4..collection_end];
                let mut i = 0;

                while i < collection.len() {
                    let header = collection[i];

                    // A zero header byte is padding — stop scanning.
                    if header == 0 {
                        break;
                    }

                    let tag = (header >> 5) & 0x07;
                    let length = (header & 0x1F) as usize;
                    i += 1;

                    if i + length > collection.len() {
                        warnings.push(Arc::new(EdidWarning::MalformedDataBlock));
                        break;
                    }

                    let block_data = &collection[i..i + length];

                    if tag == 0x01 {
                        // Audio Data Block: 3-byte Short Audio Descriptors.
                        cea_caps
                            .audio_descriptors
                            .extend(parse_audio_data_block(block_data));
                    } else if tag == 0x02 {
                        // Video Data Block: Short Video Descriptors.
                        // Standard SVD: 1 byte; bits 6:0 = VIC (1–127), bit 7 = native.
                        // Extended SVD (CTA-861-G+): if bits 6:0 == 0, the *next* byte
                        // is the full VIC number (128–255); bit 7 of the first byte = native.
                        let mut j = 0;
                        while j < block_data.len() {
                            let b = block_data[j];
                            let native = (b & 0x80) != 0;
                            let vic_low = b & 0x7F;

                            let vic = if vic_low == 0 {
                                // Extended SVD — consume one more byte.
                                j += 1;
                                match block_data.get(j).copied() {
                                    Some(0) | None => {
                                        j += 1;
                                        continue; // VIC 0 reserved
                                    }
                                    Some(v) => v,
                                }
                            } else {
                                vic_low
                            };
                            j += 1;

                            cea_caps.vics.push((vic, native));

                            if let Some(mode) = vic_to_mode(vic) {
                                let already_present = caps.supported_modes.iter().any(|m| {
                                    m.width == mode.width
                                        && m.height == mode.height
                                        && m.refresh_rate == mode.refresh_rate
                                        && m.interlaced == mode.interlaced
                                });
                                if !already_present {
                                    caps.supported_modes.push(mode);
                                }
                            }
                        }
                    } else if tag == 0x03 {
                        // Vendor-Specific Data Block: dispatch by OUI.
                        if cea_caps.hdmi_vsdb.is_none() {
                            cea_caps.hdmi_vsdb = parse_hdmi_vsdb(block_data);
                        }
                        if cea_caps.hf_vsdb.is_none() {
                            cea_caps.hf_vsdb = parse_hf_vsdb(block_data);
                        }
                    } else if tag == 0x04 {
                        // Speaker Allocation Data Block.
                        if cea_caps.speaker_allocation.is_none() {
                            cea_caps.speaker_allocation = parse_speaker_allocation(block_data);
                        }
                    } else if tag == 0x05 {
                        // VESA Display Transfer Characteristic Data Block.
                        if cea_caps.vesa_transfer_characteristic.is_none() {
                            cea_caps.vesa_transfer_characteristic =
                                parse_vesa_transfer_characteristic(block_data);
                        }
                    } else if tag == 0x07 {
                        // Extended Tag Data Block: first payload byte is the extended tag.
                        match block_data.first().copied() {
                            Some(EXT_TAG_VSVDB) => {
                                if let Some(b) = parse_vendor_specific_block(&block_data[1..]) {
                                    cea_caps.vendor_specific_video.push(b);
                                }
                            }
                            Some(EXT_TAG_VSADB) => {
                                if let Some(b) = parse_vendor_specific_block(&block_data[1..]) {
                                    cea_caps.vendor_specific_audio.push(b);
                                }
                            }
                            Some(EXT_TAG_VESA_DDDB) if cea_caps.vesa_display_device.is_none() => {
                                cea_caps.vesa_display_device =
                                    parse_vesa_display_device(&block_data[1..]);
                            }
                            Some(EXT_TAG_VTB_EXT) => {
                                if let Some(vtb) = parse_vtb_ext(&block_data[1..]) {
                                    for timing in &vtb.timings {
                                        let already_present =
                                            caps.supported_modes.iter().any(|m| {
                                                m.width == timing.width
                                                    && m.height == timing.height
                                                    && m.refresh_rate == timing.refresh_rate
                                                    && m.interlaced == timing.interlaced
                                            });
                                        if !already_present {
                                            caps.supported_modes.push(timing.clone());
                                        }
                                    }
                                    cea_caps.vtb_ext.push(vtb);
                                }
                            }
                            Some(EXT_TAG_VIDEO_CAPABILITY)
                                if cea_caps.video_capability.is_none() =>
                            {
                                cea_caps.video_capability = parse_video_capability(block_data);
                            }
                            Some(EXT_TAG_COLORIMETRY) if cea_caps.colorimetry.is_none() => {
                                cea_caps.colorimetry = parse_colorimetry(block_data);
                            }
                            Some(EXT_TAG_HDR_STATIC_METADATA)
                                if cea_caps.hdr_static_metadata.is_none() =>
                            {
                                cea_caps.hdr_static_metadata =
                                    parse_hdr_static_metadata(block_data);
                            }
                            Some(EXT_TAG_HDR_DYNAMIC_METADATA) => {
                                cea_caps
                                    .hdr_dynamic_metadata
                                    .extend(parse_hdr_dynamic_metadata(block_data));
                            }
                            Some(EXT_TAG_VIDEO_FORMAT_PREFERENCE)
                                if cea_caps.video_format_preferences.is_empty() =>
                            {
                                cea_caps.video_format_preferences =
                                    parse_video_format_preferences(block_data);
                            }
                            Some(EXT_TAG_Y420_VIDEO) => {
                                let vics = parse_y420_vdb(block_data);
                                for &vic in &vics {
                                    if let Some(mode) = vic_to_mode(vic) {
                                        let already_present =
                                            caps.supported_modes.iter().any(|m| {
                                                m.width == mode.width
                                                    && m.height == mode.height
                                                    && m.refresh_rate == mode.refresh_rate
                                                    && m.interlaced == mode.interlaced
                                            });
                                        if !already_present {
                                            caps.supported_modes.push(mode);
                                        }
                                    }
                                }
                                cea_caps.y420_vics.extend(vics);
                            }
                            Some(EXT_TAG_Y420_CAPABILITY_MAP)
                                if cea_caps.y420_capability_map.is_empty() =>
                            {
                                cea_caps.y420_capability_map =
                                    parse_y420_capability_map(block_data);
                            }
                            Some(EXT_TAG_INFOFRAME) => {
                                cea_caps
                                    .infoframe_descriptors
                                    .extend(parse_infoframe_db(block_data));
                            }
                            Some(EXT_TAG_ROOM_CONFIGURATION)
                                if cea_caps.room_configuration.is_none() =>
                            {
                                cea_caps.room_configuration = parse_room_configuration(block_data);
                            }
                            Some(EXT_TAG_SPEAKER_LOCATION) => {
                                cea_caps
                                    .speaker_locations
                                    .extend(parse_speaker_location(block_data));
                            }
                            Some(EXT_TAG_T7VTDB) => {
                                if let Some(t7) = parse_t7vtdb(&block_data[1..]) {
                                    let already_present = caps.supported_modes.iter().any(|m| {
                                        m.width == t7.mode.width
                                            && m.height == t7.mode.height
                                            && m.refresh_rate == t7.mode.refresh_rate
                                            && m.interlaced == t7.mode.interlaced
                                    });
                                    if !already_present {
                                        caps.supported_modes.push(t7.mode.clone());
                                    }
                                    cea_caps.t7_vtdb.push(t7);
                                }
                            }
                            Some(EXT_TAG_T8VTDB) => {
                                if let Some(t8) = parse_t8vtdb(&block_data[1..]) {
                                    for mode in &t8.timings {
                                        let already_present =
                                            caps.supported_modes.iter().any(|m| {
                                                m.width == mode.width
                                                    && m.height == mode.height
                                                    && m.refresh_rate == mode.refresh_rate
                                                    && m.interlaced == mode.interlaced
                                            });
                                        if !already_present {
                                            caps.supported_modes.push(mode.clone());
                                        }
                                    }
                                    cea_caps.t8_vtdb.push(t8);
                                }
                            }
                            Some(EXT_TAG_T10VTDB) => {
                                if let Some(t10) = parse_t10vtdb(&block_data[1..]) {
                                    for entry in &t10.entries {
                                        if let Ok(refresh_rate) = u8::try_from(entry.refresh_hz) {
                                            let mode = VideoMode::new(
                                                entry.width,
                                                entry.height,
                                                refresh_rate,
                                                false,
                                            );
                                            let already_present =
                                                caps.supported_modes.iter().any(|m| {
                                                    m.width == mode.width
                                                        && m.height == mode.height
                                                        && m.refresh_rate == mode.refresh_rate
                                                        && m.interlaced == mode.interlaced
                                                });
                                            if !already_present {
                                                caps.supported_modes.push(mode);
                                            }
                                        }
                                    }
                                    cea_caps.t10_vtdb.push(t10);
                                }
                            }
                            Some(EXT_TAG_HF_EEODB)
                                if cea_caps.hf_eeodb_extension_count.is_none() =>
                            {
                                cea_caps.hf_eeodb_extension_count =
                                    parse_hf_eeodb(&block_data[1..]);
                            }
                            Some(EXT_TAG_HF_SCDB) if cea_caps.hf_scdb.is_none() => {
                                cea_caps.hf_scdb = parse_hf_scdb(&block_data[1..]);
                            }
                            Some(EXT_TAG_HDMI_AUDIO) if cea_caps.hdmi_audio.is_none() => {
                                // block_data[0] = ext tag; block_data[1] = flags; block_data[2..] = SADs.
                                let multi_stream_audio =
                                    block_data.get(1).is_some_and(|&b| b & 0x08 != 0);
                                let audio_descriptors = block_data
                                    .get(2..)
                                    .map(parse_audio_data_block)
                                    .unwrap_or_default();
                                cea_caps.hdmi_audio = Some(HdmiAudioBlock::new(
                                    multi_stream_audio,
                                    audio_descriptors,
                                ));
                            }
                            _ => {}
                        }
                    }

                    i += length;
                }
            }

            // Parse Detailed Timing Descriptors from byte dtd_offset through byte 126.
            // When dtd_offset == 0 there are no DTDs (the spec says no timing data present).
            if (4..=110).contains(&dtd_offset) {
                let mut offset = dtd_offset;
                while offset + 18 <= 127 {
                    decode_dtd_slot(&ext[offset..offset + 18], caps);
                    offset += 18;
                }
            }
        } // end for ext in blocks

        caps.set_extension_data(0x02, cea_caps);
    }
}

/// Extracts video modes from a CEA-861 extension block into `sink`.
///
/// This is the allocation-free counterpart to [`Cea861Handler::process`]: it produces
/// [`VideoMode`][crate::VideoMode] entries (SVDs, DTDs, and — in `alloc`/`std` builds —
/// VTB-EXT/T7VTDB/T8VTDB/T10VTDB/Y420 VDB entries) without requiring heap allocation.
pub(crate) fn cea861_process_into_sink(ext: &[u8; 128], sink: &mut dyn ModeSink) {
    let dtd_offset = ext[2] as usize;
    let collection_end = if dtd_offset == 0 {
        128
    } else {
        dtd_offset.min(128)
    };

    if collection_end > 4 {
        let collection = &ext[4..collection_end];
        let mut i = 0;

        while i < collection.len() {
            let header = collection[i];
            if header == 0 {
                break;
            }

            let tag = (header >> 5) & 0x07;
            let length = (header & 0x1F) as usize;
            i += 1;

            if i + length > collection.len() {
                sink.push_warning(EdidWarning::MalformedDataBlock);
                break;
            }

            let block_data = &collection[i..i + length];

            if tag == 0x02 {
                // Video Data Block: Short Video Descriptors.
                let mut j = 0;
                while j < block_data.len() {
                    let b = block_data[j];
                    let vic_low = b & 0x7F;

                    let vic = if vic_low == 0 {
                        j += 1;
                        match block_data.get(j).copied() {
                            Some(0) | None => {
                                j += 1;
                                continue;
                            }
                            Some(v) => v,
                        }
                    } else {
                        vic_low
                    };
                    j += 1;

                    if let Some(mode) = vic_to_mode(vic) {
                        sink.push_mode(mode);
                    }
                }
            } else if tag == 0x07 {
                // Extended Tag Data Block — mode-producing sub-types.
                #[cfg(any(feature = "alloc", feature = "std"))]
                match block_data.first().copied() {
                    Some(EXT_TAG_VTB_EXT) => {
                        if let Some(vtb) = parse_vtb_ext(&block_data[1..]) {
                            for timing in vtb.timings {
                                sink.push_mode(timing);
                            }
                        }
                    }
                    Some(EXT_TAG_Y420_VIDEO) => {
                        for &vic in &parse_y420_vdb(block_data) {
                            if let Some(mode) = vic_to_mode(vic) {
                                sink.push_mode(mode);
                            }
                        }
                    }
                    Some(EXT_TAG_T7VTDB) => {
                        if let Some(t7) = parse_t7vtdb(&block_data[1..]) {
                            sink.push_mode(t7.mode);
                        }
                    }
                    Some(EXT_TAG_T8VTDB) => {
                        if let Some(t8) = parse_t8vtdb(&block_data[1..]) {
                            for mode in t8.timings {
                                sink.push_mode(mode);
                            }
                        }
                    }
                    Some(EXT_TAG_T10VTDB) => {
                        if let Some(t10) = parse_t10vtdb(&block_data[1..]) {
                            for entry in &t10.entries {
                                if let Ok(refresh_rate) = u8::try_from(entry.refresh_hz) {
                                    sink.push_mode(VideoMode::new(
                                        entry.width,
                                        entry.height,
                                        refresh_rate,
                                        false,
                                    ));
                                }
                            }
                        }
                    }
                    _ => {}
                }
            }

            i += length;
        }
    }

    // Parse Detailed Timing Descriptors.
    if (4..=110).contains(&dtd_offset) {
        let mut offset = dtd_offset;
        while offset + 18 <= 127 {
            decode_dtd_slot_into_sink(&ext[offset..offset + 18], sink);
            offset += 18;
        }
    }
}

impl StaticExtensionHandler for Cea861Handler {
    fn tag(&self) -> u8 {
        0x02
    }

    fn process(&self, blocks: &[&[u8; 128]], ctx: &mut StaticContext<'_>) {
        for block in blocks {
            cea861_process_into_sink(block, ctx);
        }
    }
}

/// Pre-built static reference to the built-in CEA-861 handler.
///
/// Suitable for passing to [`parse_edid`][crate::parse_edid] as the `KnownExtensions`
/// argument, and to [`capabilities_from_edid_static`][crate::capabilities_from_edid_static]
/// as a single-element slice.
pub static CEA861_HANDLER: &dyn StaticExtensionHandler = &Cea861Handler;

#[cfg(test)]
mod test_static {
    use super::*;
    use crate::model::extension::StaticExtensionHandler;

    #[test]
    fn test_static_handler_tag() {
        assert_eq!(StaticExtensionHandler::tag(&Cea861Handler), 0x02);
    }
}

#[cfg(test)]
#[cfg(any(feature = "alloc", feature = "std"))]
mod tests {
    use super::*;
    use crate::model::capabilities::DisplayCapabilities;

    #[test]
    fn test_audio_flag() {
        let mut ext = [0u8; 128];
        ext[3] = Cea861Flags::BASIC_AUDIO.bits();

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert!(cea.flags.contains(Cea861Flags::BASIC_AUDIO));
    }

    #[test]
    fn test_no_audio_flag() {
        let ext = [0u8; 128];

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert!(!cea.flags.contains(Cea861Flags::BASIC_AUDIO));
    }

    #[test]
    fn test_flags_parsing() {
        let flags = Cea861Flags::from_bits_truncate(0xF0);
        assert!(flags.contains(Cea861Flags::UNDERSCAN));
        assert!(flags.contains(Cea861Flags::BASIC_AUDIO));
        assert!(flags.contains(Cea861Flags::YCBCR_444));
        assert!(flags.contains(Cea861Flags::YCBCR_422));
    }

    #[test]
    fn test_malformed_data_block_warns() {
        let mut ext = [0u8; 128];
        // dtd_offset = 10: data block collection occupies bytes 4–9 (6 bytes).
        ext[2] = 10;
        // Write a block header claiming length = 8, which extends past the 6-byte collection.
        // tag = 1 (Audio), length = 8 → header = (1 << 5) | 8 = 0x28
        ext[4] = (1 << 5) | 8;

        let mut caps = DisplayCapabilities::default();
        let mut warnings = Vec::new();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut warnings);

        assert!(
            warnings
                .iter()
                .any(|w| (**w).downcast_ref::<EdidWarning>()
                    == Some(&EdidWarning::MalformedDataBlock))
        );
    }

    #[test]
    fn test_svd_parsing() {
        let mut ext = [0u8; 128];
        // dtd_offset = 8: data block collection occupies bytes 4–7
        ext[2] = 8;
        ext[3] = Cea861Flags::BASIC_AUDIO.bits();
        // Video Data Block: tag=2 (010), length=3  →  header = (2 << 5) | 3 = 0x43
        ext[4] = (2 << 5) | 3;
        ext[5] = 0x90; // VIC 16, native flag set  (0x80 | 16)
        ext[6] = 0x04; // VIC 4
        ext[7] = 0x01; // VIC 1

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert_eq!(cea.vics, vec![(16, true), (4, false), (1, false)]);

        // All three VICs should produce entries in supported_modes
        assert_eq!(caps.supported_modes.len(), 3);
        assert!(
            caps.supported_modes.iter().any(|m| m.width == 1920
                && m.height == 1080
                && m.refresh_rate == 60
                && !m.interlaced)
        );
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 1280 && m.height == 720 && m.refresh_rate == 60)
        );
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 640 && m.height == 480 && m.refresh_rate == 60)
        );
    }

    #[test]
    fn test_svd_dedup() {
        // If a mode already appears in supported_modes it should not be added again.
        let mut ext = [0u8; 128];
        // dtd_offset = 7: collection = ext[4..7] = 3 bytes (1 header + 2 SVDs)
        ext[2] = 7;
        // Video Data Block with two SVDs for the same VIC
        ext[4] = (2 << 5) | 2; // tag=2, length=2
        ext[5] = 0x10; // VIC 16
        ext[6] = 0x90; // VIC 16, native — duplicate

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        // Both SVD bytes are recorded in vics…
        assert_eq!(cea.vics.len(), 2);
        // …but supported_modes has only one entry for 1920×1080p60
        assert_eq!(
            caps.supported_modes
                .iter()
                .filter(|m| m.width == 1920 && m.height == 1080 && m.refresh_rate == 60)
                .count(),
            1
        );
    }

    #[test]
    fn test_svd_byte_0x80_is_skipped() {
        // A standard SVD byte uses bit 7 as the native flag and bits 6-0 as the
        // VIC number. Byte 0x80 → native=true, vic=0. VIC 0 is reserved and must
        // be skipped: it should not appear in vics and must not produce a mode.
        let mut ext = [0u8; 128];
        ext[2] = 6;
        ext[4] = (2 << 5) | 1; // tag=2, length=1
        ext[5] = 0x80; // native flag set, vic=0

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert!(cea.vics.is_empty());
        assert!(caps.supported_modes.is_empty());
    }

    /// Builds a minimal valid 18-byte DTD for 2560×1440@144Hz into `ext` starting at `offset`.
    ///
    /// Pixel clock = 583.92 MHz → stored as 58392 (little-endian units of 10 kHz).
    /// Htotal = 2720 (active 2560 + blank 160), Vtotal = 1481 (active 1440 + blank 41).
    /// 58392 * 10000 / (2720 * 1481) = 144.8 → rounds to 144.
    fn write_dtd_2560x1440_144(ext: &mut [u8; 128], offset: usize) {
        let clk: u16 = 58392; // 10 kHz units
        ext[offset] = (clk & 0xFF) as u8;
        ext[offset + 1] = (clk >> 8) as u8;
        // hactive=2560, hblank=160
        ext[offset + 2] = (2560 & 0xFF) as u8; // hactive low
        ext[offset + 3] = (160 & 0xFF) as u8; // hblank low
        ext[offset + 4] = ((2560 >> 4) & 0xF0) as u8; // hblank high nibble = 0 (160 < 256)
        // vactive=1440, vblank=41
        ext[offset + 5] = (1440 & 0xFF) as u8; // vactive low
        ext[offset + 6] = (41 & 0xFF) as u8; // vblank low
        ext[offset + 7] = ((1440 >> 4) & 0xF0) as u8; // vblank high nibble = 0 (41 < 256)
        // bytes 8-16: sync/image (leave zero — minimal)
        // byte 17: digital separate sync, both polarities positive
        ext[offset + 17] = 0x1E; // bit4=digital, bit3=separate, bit2=Vpos, bit1=Hpos
    }

    #[test]
    fn test_cea_dtd_parsed() {
        let mut ext = [0u8; 128];
        // No data block collection; DTDs start at offset 4.
        ext[2] = 4; // dtd_offset = 4
        write_dtd_2560x1440_144(&mut ext, 4);

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 2560 && m.height == 1440 && m.refresh_rate == 144),
            "2560×1440@144 not found in supported_modes"
        );
    }

    #[test]
    fn test_cea_dtd_zero_clock_skipped() {
        // A DTD slot whose first two bytes are 0x00 is a monitor descriptor and must be skipped.
        let mut ext = [0u8; 128];
        ext[2] = 4; // dtd_offset = 4 — no data blocks, DTDs start immediately
        // ext[4..22] stays all-zero → zero pixel clock → skipped

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        assert!(caps.supported_modes.is_empty());
    }

    #[test]
    fn test_speaker_allocation_parsed() {
        let mut ext = [0u8; 128];
        // Speaker Allocation Data Block: tag=4, length=3 → header = (4 << 5) | 3 = 0x83
        ext[2] = 8;
        ext[4] = (4 << 5) | 3;
        ext[5] = 0x07; // FL_FR | LFE1 | FC
        ext[6] = 0x00;
        ext[7] = 0x00;

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        let sa = cea.speaker_allocation.unwrap();
        assert!(sa.channels.contains(SpeakerAllocationFlags::FL_FR));
        assert!(sa.channels.contains(SpeakerAllocationFlags::LFE1));
        assert!(sa.channels.contains(SpeakerAllocationFlags::FC));
    }

    #[test]
    fn test_y420_vdb_adds_modes_and_vics() {
        let mut ext = [0u8; 128];
        // Y420 VDB extended block: outer tag=7, length=2 → header = (7 << 5) | 2 = 0xE2
        // payload: extended tag 0x0E, VIC 96 (2160p50)
        ext[2] = 8;
        ext[4] = (7 << 5) | 2;
        ext[5] = 0x0E; // EXT_TAG_Y420_VIDEO
        ext[6] = 96; // VIC 96

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert_eq!(cea.y420_vics, vec![96]);
        // VIC 96 = 3840×2160@50Hz — should appear in supported_modes
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 3840 && m.height == 2160 && m.refresh_rate == 50)
        );
    }

    #[test]
    fn test_extended_svd_vic193() {
        // Extended SVD encoding for VIC 193 (5120×2160p120):
        // first byte D6:D0 = 0 (native flag in D7), second byte = 193.
        let mut ext = [0u8; 128];
        // Video Data Block: tag=2, length=2 → header = 0x42
        ext[2] = 8;
        ext[4] = (2 << 5) | 2;
        ext[5] = 0x00; // bits 6:0 = 0 → extended SVD; native = false
        ext[6] = 193; // full VIC

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert_eq!(cea.vics, vec![(193, false)]);
        // VIC 193 = 5120×2160p120 — should appear in supported_modes
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 5120 && m.height == 2160 && m.refresh_rate == 120)
        );
    }

    #[test]
    fn test_extended_svd_native_flag() {
        // Extended SVD with native flag set: first byte = 0x80 (native=true, VIC bits=0).
        let mut ext = [0u8; 128];
        ext[2] = 8;
        ext[4] = (2 << 5) | 2;
        ext[5] = 0x80; // native=true, bits 6:0 = 0 → extended SVD
        ext[6] = 196; // VIC 196

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert_eq!(cea.vics, vec![(196, true)]);
    }

    #[test]
    fn test_extended_svd_truncated_is_skipped() {
        // Extended SVD marker as the very last byte — second byte missing.
        let mut ext = [0u8; 128];
        ext[2] = 6;
        ext[4] = (2 << 5) | 1; // tag=2, length=1 — only the marker byte, no VIC byte
        ext[5] = 0x00; // extended SVD marker with no following byte

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert!(cea.vics.is_empty());
    }

    #[test]
    fn test_y420_capability_map_stored() {
        let mut ext = [0u8; 128];
        // Y420 Capability Map: outer tag=7, length=2 → 0xE2; ext tag 0x0F, bitmap 0b00000101
        ext[2] = 8;
        ext[4] = (7 << 5) | 2;
        ext[5] = 0x0F; // EXT_TAG_Y420_CAPABILITY_MAP
        ext[6] = 0b0000_0101;

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        assert_eq!(cea.y420_capability_map, vec![0b0000_0101]);
    }

    #[test]
    fn test_hdmi_audio_block_with_msa_and_sads() {
        // HDMI Audio Data Block: outer tag=7, length=8 → (7<<5)|8 = 0xE8
        // payload: ext tag 0x12, flags byte (MSA=bit3 set), then two SADs (6 bytes) = 8 bytes
        // SAD 1: LPCM (AFC=1), 2ch, 44.1kHz+48kHz, 16-bit
        // SAD 2: AC-3 (AFC=2), 6ch, 48kHz, max bitrate 640kbps
        let mut ext = [0u8; 128];
        // collection = ext[4..13] = 9 bytes (1 header + 8 payload)
        ext[2] = 13;
        ext[4] = (7 << 5) | 8; // outer tag=7, length=8
        ext[5] = 0x12; // EXT_TAG_HDMI_AUDIO
        ext[6] = 0x08; // flags: MSA set (bit 3)
        // SAD 1: LPCM 2ch
        ext[7] = (1 << 3) | 1; // AFC=1 (LPCM), max_ch=2 → (AFC<<3)|(ch-1) = 0x09
        ext[8] = 0x06; // 44.1kHz (bit1) + 48kHz (bit2)
        ext[9] = 0x01; // 16-bit
        // SAD 2: AC-3 6ch
        ext[10] = (2 << 3) | 5; // AFC=2 (AC-3), max_ch=6 → 0x15
        ext[11] = 0x04; // 48kHz only
        ext[12] = 0x50; // max bitrate = 0x50 * 8 = 640 kbps

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        let hdmi_audio = cea.hdmi_audio.as_ref().unwrap();
        assert!(hdmi_audio.multi_stream_audio);
        assert_eq!(hdmi_audio.audio_descriptors.len(), 2);
        assert!(matches!(
            hdmi_audio.audio_descriptors[0].format,
            AudioFormat::Lpcm
        ));
        assert_eq!(hdmi_audio.audio_descriptors[0].max_channels, 2);
        assert!(matches!(
            hdmi_audio.audio_descriptors[1].format,
            AudioFormat::Ac3
        ));
        assert_eq!(hdmi_audio.audio_descriptors[1].max_channels, 6);
    }

    #[test]
    fn test_hdmi_audio_block_no_msa() {
        let mut ext = [0u8; 128];
        // collection = ext[4..10] = 6 bytes (1 header + 5 payload: ext_tag+flags+3-byte SAD)
        ext[2] = 10;
        ext[4] = (7 << 5) | 5; // outer tag=7, length=5
        ext[5] = 0x12; // EXT_TAG_HDMI_AUDIO
        ext[6] = 0x00; // flags: MSA not set
        // One LPCM SAD
        ext[7] = (1 << 3) | 1;
        ext[8] = 0x04;
        ext[9] = 0x01;

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        let hdmi_audio = cea.hdmi_audio.as_ref().unwrap();
        assert!(!hdmi_audio.multi_stream_audio);
        assert_eq!(hdmi_audio.audio_descriptors.len(), 1);
    }

    #[test]
    fn test_hdmi_audio_block_flags_only() {
        // Block with no SADs — just ext tag + flags byte.
        let mut ext = [0u8; 128];
        ext[2] = 7;
        ext[4] = (7 << 5) | 2; // length=2: ext tag + flags, no SADs
        ext[5] = 0x12;
        ext[6] = 0x08; // MSA set

        let mut caps = DisplayCapabilities::default();
        ExtensionHandler::process(&Cea861Handler, &[&ext], &mut caps, &mut Vec::new());

        let cea = caps.get_extension_data::<Cea861Capabilities>(0x02).unwrap();
        let hdmi_audio = cea.hdmi_audio.as_ref().unwrap();
        assert!(hdmi_audio.multi_stream_audio);
        assert!(hdmi_audio.audio_descriptors.is_empty());
    }
}
