//! MPEG-2 Program Stream (PS) demuxer.
//!
//! DVDs use MPEG-2 Program Stream, which has:
//! - Pack headers (00 00 01 BA) with SCR timestamps
//! - PES packets (00 00 01 [stream_id]) with variable length
//! - System headers (00 00 01 BB)
//! - Program end code (00 00 01 B9)
//!
//! Stream IDs:
//! - 0xE0-0xEF: video (usually 0xE0)
//! - 0xC0-0xDF: MPEG audio
//! - 0xBD: private stream 1 (AC3, DTS, LPCM, subtitles via sub-stream ID)

use super::codec::startcode::find_start_code;

/// Pack header start code suffix.
const PACK_HEADER_ID: u8 = 0xBA;

/// System header start code suffix.
const SYSTEM_HEADER_ID: u8 = 0xBB;

/// Program end start code suffix.
const PROGRAM_END_ID: u8 = 0xB9;

/// Private stream 1 (AC3, DTS, LPCM, subtitles).
const PRIVATE_STREAM_1: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_1;
/// Private stream 2 (0xBF) — DVD navigation (PCI/DSI). Carries no muxable
/// elementary stream; expected to be dropped on every disc.
const PRIVATE_STREAM_2: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_2;
/// Extended stream id (0xFD) — the H.222.0 escape whereby the real stream id is
/// the `stream_id_extension` carried in the PES extension. HD-DVD `.evo` puts its
/// VC-1 video (and HD audio) here (Shaun of the Dead: VC-1 on `0xFD` ext `0x55`);
/// a transport stream never uses it. The elementary-stream bytes follow the PES
/// header exactly like any other PES — only the routing key differs.
const EXTENDED_STREAM_ID: u8 = 0xFD;

/// Hard cap on the demuxer's reassembly buffer. A length-0 (unbounded) video
/// PES is delimited by the next PS-layer boundary; if a corrupt stream declares
/// an unbounded PES and never follows it with a boundary, `feed()` would
/// otherwise accumulate the entire input. Past this cap we force the in-progress
/// unbounded PES to flush at the buffer end so untrusted input cannot drive
/// unbounded allocation. A real DVD pack/PES is at most a few KB; this leaves
/// generous slack while still bounding worst-case memory.
const MAX_PS_BUFFER: usize = 4 * 1024 * 1024;

/// A demuxed PES packet from the Program Stream.
#[derive(Debug, Clone)]
pub struct PsPacket {
    /// PES stream ID (0xE0 for video, 0xC0 for audio, 0xBD for private, etc.).
    pub stream_id: u8,
    /// Sub-stream ID for private stream 1 (AC3: 0x80-0x87, DTS: 0x88-0x8F,
    /// LPCM: 0xA0-0xA7, subtitles: 0x20-0x3F).
    pub sub_stream_id: Option<u8>,
    /// Presentation timestamp in 90kHz ticks.
    pub pts: Option<u64>,
    /// Decode timestamp in 90kHz ticks.
    pub dts: Option<u64>,
    /// Elementary stream payload data.
    pub data: Vec<u8>,
    /// Source position of this PES's first ES byte, stamped at the demux seam
    /// from the producer's known stream offset. `None` when the demuxer was fed
    /// without a base offset.
    pub source: Option<crate::pes::SourcePos>,
}

/// Canonical DVD video PID. DVD-Video carries a single MPEG-2 video
/// elementary stream; both the scanner and the muxer use this PID.
pub const DVD_VIDEO_PID: u16 = 0xE0;

/// Canonical PID for a `private_stream_1` audio stream identified by its
/// on-wire sub-stream id. Returns `None` for sub-ids outside the AC-3 /
/// DTS / LPCM audio ranges.
///
/// The PID is `0xBD00 | sub_stream_id`, which is unique per sub-stream id
/// (AC-3 / DTS `0x80..=0x8F`, LPCM `0xA0..=0xA7`). Unlike the old
/// per-codec relative arithmetic, distinct sub-ids therefore always yield
/// distinct PIDs — so a mixed-codec title (e.g. AC-3 + DTS, whose sub-ids
/// are 0x80 and 0x88) can never collide on one PID. This is the single
/// source of truth shared with `Disc::scan_dvd_titles`
/// (`src/disc/dvd.rs`), which sets each `AudioStream.pid` from the same
/// function so demuxer output routes through the title's `pid_to_track`.
///
/// HD-DVD (`.evo` Enhanced VOB) carries Dolby Digital Plus (E-AC-3) on
/// `private_stream_1` sub-stream ids `0xC0..=0xC7` — a range DVD never uses
/// (DVD audio is `0x80..=0x8F` / `0xA0..=0xA7`), so admitting it here is purely
/// additive and cannot change any DVD mapping. The PID is `0xBD00 | sub` just
/// like the DVD audio ranges, so a mixed HD-DVD title (four DD+ tracks
/// `0xC0..0xC3`) routes each track to its own distinct PID.
pub fn dvd_audio_pid(sub_stream_id: u8) -> Option<u16> {
    match sub_stream_id {
        0x80..=0x8F | 0xA0..=0xA7 | 0xC0..=0xC7 => Some(0xBD00 | sub_stream_id as u16),
        _ => None,
    }
}

/// Canonical PID for a VobSub subtitle stream identified by its on-wire
/// sub-stream id (`0x20..=0x3F`). The PID is the sub-id itself (identity),
/// which never overlaps the `0xBD..` audio PID space.
pub fn dvd_subtitle_pid(sub_stream_id: u8) -> Option<u16> {
    match sub_stream_id {
        0x20..=0x3F => Some(sub_stream_id as u16),
        _ => None,
    }
}

/// Canonical PID for an HD-DVD extended-stream-id (`0xFD`) stream, keyed by its
/// `stream_id_extension`: `0xFD00 | ext`. Disjoint from the DVD video (`0xE0`) and
/// `private_stream_1` (`0xBD00..`) PID spaces, so several elementary streams
/// multiplexed on `0xFD` (VC-1 video, MLP/TrueHD audio) never collide. The
/// scanner's head probe and `PsPacket::dvd_pid` derive the same PID from the same
/// `stream_id_extension`, so demux output routes through the title's
/// `pid_to_track`.
pub fn hddvd_extended_pid(stream_id_extension: u8) -> u16 {
    0xFD00 | stream_id_extension as u16
}

impl PsPacket {
    /// Map this packet to the canonical DVD PID assigned by
    /// `Disc::scan_dvd_titles` (`src/disc/dvd.rs`), so demux output can
    /// be looked up in the title's `pid_to_track` map.
    ///
    /// Routes by the REAL on-wire `(stream_id, sub_stream_id)` via the
    /// shared [`dvd_audio_pid`] / [`dvd_subtitle_pid`] tables the scanner
    /// also uses — never per-codec relative arithmetic, which collided on
    /// mixed-codec audio (AC-3 0x80 and DTS 0x88 both mapping to 0xBD00).
    ///
    /// Returns `None` for stream/sub-stream combinations the DVD title
    /// scanner does not assign a PID to (e.g. MPEG audio 0xC0-0xDF,
    /// private stream 2, unrecognized sub-stream ranges). The caller is
    /// expected to WARN-and-drop in that case rather than silently
    /// mis-routing the packet.
    pub fn dvd_pid(&self) -> Option<u16> {
        match self.stream_id {
            crate::consts::pes_stream_id::VIDEO..=0xEF => Some(DVD_VIDEO_PID),
            PRIVATE_STREAM_1 => {
                let sub = self.sub_stream_id?;
                dvd_audio_pid(sub).or_else(|| dvd_subtitle_pid(sub))
            }
            // HD-DVD extended-stream-id (0xFD): route by the stream_id_extension
            // (carried in `sub_stream_id`) to a distinct `0xFD00 | ext` PID, so a
            // disc that puts several elementary streams on 0xFD keeps them apart.
            // The codec (VC-1 etc.) is decided by the scanner's head probe, not
            // here — this only assigns a stable routing key.
            EXTENDED_STREAM_ID => self.sub_stream_id.map(hddvd_extended_pid),
            _ => None,
        }
    }

    /// Whether this is a DVD navigation packet (private_stream_2, 0xBF —
    /// PCI/DSI). These carry no muxable elementary stream and are EXPECTED to
    /// be dropped on every DVD, so a per-packet WARN is noise: the mux loops
    /// count them and emit one finalize summary instead. A `dvd_pid()` of
    /// `None` for any OTHER stream_id is unexpected (a possibly-dropped real
    /// stream) and stays an individual WARN.
    pub fn is_nav(&self) -> bool {
        self.stream_id == PRIVATE_STREAM_2
    }
}

/// MPEG-2 Program Stream demuxer.
///
/// Accepts raw PS bytes via `feed()` and produces demuxed PES packets.
/// Handles non-aligned input by buffering leftover bytes between calls.
pub struct PsDemuxer {
    buffer: Vec<u8>,
    /// Absolute source byte offset of `buffer[0]` — the running base that turns
    /// an in-buffer unit position into a [`crate::pes::SourcePos`]. Advanced as
    /// the buffer drains. `has_base` gates stamping so non-provenance callers
    /// stay byte-identical.
    buffer_base: u64,
    has_base: bool,
}

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

impl PsDemuxer {
    /// Create a new Program Stream demuxer.
    pub fn new() -> Self {
        Self {
            buffer: Vec::with_capacity(64 * 1024),
            buffer_base: 0,
            has_base: false,
        }
    }

    /// Feed raw MPEG-2 PS bytes, returning any completely parsed PES packets.
    pub fn feed(&mut self, data: &[u8]) -> Vec<PsPacket> {
        self.buffer.extend_from_slice(data);
        self.extract_packets(false)
    }

    /// Like [`feed`](Self::feed) but records the absolute source byte offset of
    /// `data[0]`, so every PES this call completes is stamped with a
    /// [`crate::pes::SourcePos`]. The provenance-stamping entry point; the
    /// highway calls this with each batch's known source offset. The base must
    /// be the offset of the FIRST byte appended (i.e. of `data[0]`), which lines
    /// up with the current buffer tail.
    pub fn feed_at(&mut self, base_offset: u64, data: &[u8]) -> Vec<PsPacket> {
        if !self.has_base {
            // First base seen: the offset of data[0] is base_offset, and data[0]
            // lands at buffer[buffer.len()], so buffer[0] is base_offset minus
            // the bytes already buffered.
            self.buffer_base = base_offset.saturating_sub(self.buffer.len() as u64);
            self.has_base = true;
        }
        self.buffer.extend_from_slice(data);
        self.extract_packets(false)
    }

    /// Flush remaining buffered data, returning any final PES packets.
    pub fn flush(&mut self) -> Vec<PsPacket> {
        // At EOF, an unbounded (length 0) PES with no trailing start code is
        // a complete-but-unterminated final packet — emit it rather than
        // dropping the tail of the last frame. Genuinely incomplete packets
        // (a length-bounded PES short of its declared size) are still
        // discarded.
        let packets = self.extract_packets(true);
        self.buffer.clear();
        packets
    }

    /// Scan the buffer for complete start-code-delimited units and parse
    /// them. When `flushing` is true, a trailing unbounded PES that has no
    /// following start code is emitted using the rest of the buffer as its
    /// payload (EOF terminates it).
    fn extract_packets(&mut self, flushing: bool) -> Vec<PsPacket> {
        let mut packets = Vec::with_capacity(4);
        let mut pos = 0;

        while let Some(sc) = find_start_code(&self.buffer, pos) {
            if sc + 3 >= self.buffer.len() {
                // Not enough bytes to read the start code ID.
                break;
            }

            let code = self.buffer[sc + 3];

            match code {
                PROGRAM_END_ID => {
                    // 00 00 01 B9 — 4 bytes, no payload.
                    pos = sc + 4;
                }
                PACK_HEADER_ID => {
                    // Pack header: need at least 14 bytes for MPEG-2 pack.
                    if sc + 14 > self.buffer.len() {
                        break; // wait for more data
                    }
                    // DVD-Video is always MPEG-2 PS, so every 0xBA is treated
                    // as a 14-byte MPEG-2 pack: the low 3 bits of byte 13 are
                    // pack_stuffing_length. (An MPEG-1 pack would be 12 bytes
                    // with no stuffing field, but DVD never emits one.)
                    let stuffing = (self.buffer[sc + 13] & 0x07) as usize;
                    let pack_len = 14 + stuffing;
                    if sc + pack_len > self.buffer.len() {
                        break;
                    }
                    pos = sc + pack_len;
                }
                SYSTEM_HEADER_ID => {
                    // System header: 00 00 01 BB [length:2] ...
                    if sc + 6 > self.buffer.len() {
                        break;
                    }
                    let header_len =
                        ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize;
                    let total = 6 + header_len;
                    if sc + total > self.buffer.len() {
                        break;
                    }
                    pos = sc + total;
                }
                id if is_pes_stream_id(id) => {
                    // PES packet: 00 00 01 [stream_id] [length:2] ...
                    if sc + 6 > self.buffer.len() {
                        break;
                    }
                    let pes_packet_len =
                        ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize;

                    // Total bytes = 6 (start code + stream_id + length) + pes_packet_len.
                    // A length of 0 means unbounded (video streams); in that
                    // case the packet runs to the next PS-LAYER boundary (pack /
                    // system header / program end / next PES), NOT the next raw
                    // start code — the video ES payload is itself full of
                    // 00 00 01 xx codes that would otherwise cut the PES short.
                    let end = if pes_packet_len == 0 {
                        match find_ps_boundary(&self.buffer, sc + 4) {
                            Some(next) => next,
                            // At EOF the rest of the buffer is this PES's
                            // payload — emit it.
                            None if flushing => self.buffer.len(),
                            None => {
                                // No boundary buffered yet. Normally wait for
                                // more data, but a corrupt stream could declare
                                // an unbounded PES followed by endless non-
                                // boundary bytes — bounding the buffer here
                                // stops untrusted input forcing unbounded
                                // allocation. Past the cap, flush what we have.
                                if self.buffer.len() - sc > MAX_PS_BUFFER {
                                    self.buffer.len()
                                } else {
                                    break; // wait for more data
                                }
                            }
                        }
                    } else {
                        let e = sc + 6 + pes_packet_len;
                        if e > self.buffer.len() {
                            break; // wait for more data
                        }
                        e
                    };

                    if let Some(mut pkt) = parse_pes_packet(&self.buffer[sc..end]) {
                        if self.has_base {
                            pkt.source =
                                Some(crate::pes::SourcePos::at_byte(self.buffer_base + sc as u64));
                        }
                        packets.push(pkt);
                    }
                    pos = end;
                }
                _ => {
                    // Unknown start code — skip past it.
                    pos = sc + 4;
                }
            }
        }

        if pos > 0 {
            self.buffer.drain(..pos);
            // Advance the absolute base past the drained bytes so subsequent
            // units stamp from the correct offset.
            if self.has_base {
                self.buffer_base += pos as u64;
            }
        }

        packets
    }
}

/// Find the next PS-layer unit boundary at or after `from`: a start code whose
/// ID byte is a pack (0xBA), system header (0xBB), program-end (0xB9), or a
/// payload-carrying PES stream ID (0xBD..=0xEF).
///
/// A length-0 (unbounded) video PES must be delimited by the next PS-layer unit
/// — NOT by the next raw `00 00 01`. The MPEG-2 video elementary stream inside
/// the PES is itself full of `00 00 01 xx` start codes (picture 0x00, slices
/// 0x01..=0xAF, GOP 0xB8, sequence 0xB3); a plain start-code scan would cut the
/// PES inside its own payload and re-scan the discarded video bytes as bogus PS
/// units. Restricting the search to PS-layer IDs (>= 0xB9, excluding the video
/// ES codes below it) frames the unbounded PES at the right boundary.
fn find_ps_boundary(data: &[u8], from: usize) -> Option<usize> {
    let mut pos = from;
    while let Some(sc) = find_start_code(data, pos) {
        if sc + 3 >= data.len() {
            return None;
        }
        let id = data[sc + 3];
        if id == PACK_HEADER_ID
            || id == SYSTEM_HEADER_ID
            || id == PROGRAM_END_ID
            || is_pes_stream_id(id)
        {
            return Some(sc);
        }
        pos = sc + 4;
    }
    None
}

/// Check whether a start code byte is a valid PES stream ID that carries payload.
fn is_pes_stream_id(id: u8) -> bool {
    // Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD,
    // private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc. — plus the HD-DVD
    // extended-stream-id (0xFD), which carries VC-1 video / HD audio.
    crate::consts::pes_stream_id::PAYLOAD_RANGE.contains(&id) || id == EXTENDED_STREAM_ID
}

/// For an extended-stream-id (`0xFD`) PES, walk the optional PES-header fields to
/// the PES extension and read the 7-bit `stream_id_extension` — the real stream
/// id. `data` starts at the PES start code; the optional fields live in
/// `data[9..header_end]` (all bounds-checked against `header_end`). Returns `None`
/// if the extension is absent or malformed.
fn parse_stream_id_extension(data: &[u8], flags2: u8, header_end: usize) -> Option<u8> {
    let get = |p: usize| -> Option<u8> {
        if p < header_end {
            data.get(p).copied()
        } else {
            None
        }
    };
    let mut pos = 9usize;
    let pts_dts = (flags2 >> 6) & 0x03;
    if pts_dts & 0x02 != 0 {
        pos += 5; // PTS
    }
    if pts_dts == 0x03 {
        pos += 5; // DTS
    }
    if flags2 & 0x20 != 0 {
        pos += 6; // ESCR
    }
    if flags2 & 0x10 != 0 {
        pos += 3; // ES_rate
    }
    if flags2 & 0x08 != 0 {
        pos += 1; // DSM_trick_mode
    }
    if flags2 & 0x04 != 0 {
        pos += 1; // additional_copy_info
    }
    if flags2 & 0x02 != 0 {
        pos += 2; // PES_CRC
    }
    if flags2 & 0x01 == 0 {
        return None; // no PES_extension
    }
    let ext_flags = get(pos)?;
    pos += 1;
    if ext_flags & 0x80 != 0 {
        pos += 16; // PES_private_data
    }
    if ext_flags & 0x40 != 0 {
        // pack_header_field: 1-byte length + that many bytes.
        pos += 1 + get(pos)? as usize;
    }
    if ext_flags & 0x20 != 0 {
        pos += 2; // program_packet_sequence_counter
    }
    if ext_flags & 0x10 != 0 {
        pos += 2; // P-STD_buffer
    }
    if ext_flags & 0x01 == 0 {
        return None; // no PES_extension_flag_2
    }
    // PES_extension_field_length (7 bits, marker in the top bit), then the
    // stream_id_extension byte: present when its top bit (the extension flag) is 0.
    let _field_len = get(pos)? & 0x7F;
    pos += 1;
    let b = get(pos)?;
    (b & 0x80 == 0).then_some(b & 0x7F)
}

/// Parse a single PES packet from a byte slice that starts at the start code.
fn parse_pes_packet(data: &[u8]) -> Option<PsPacket> {
    // Minimum: 00 00 01 [id] [len:2] = 6 bytes
    if data.len() < 6 {
        return None;
    }
    if data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x01 {
        return None;
    }

    let stream_id = data[3];

    // Padding stream — skip entirely.
    if stream_id == crate::consts::pes_stream_id::PADDING_STREAM {
        return None;
    }

    // Streams without standard PES header extension.
    if stream_id == PRIVATE_STREAM_2 {
        let payload = if data.len() > 6 { &data[6..] } else { &[] };
        return Some(PsPacket {
            stream_id,
            sub_stream_id: None,
            pts: None,
            dts: None,
            data: payload.to_vec(),
            // Stamped by the demuxer (extract_packets) when a source base is
            // threaded; the free function has no absolute offset of its own.
            source: None,
        });
    }

    // Standard PES header: [6]=flags1, [7]=flags2, [8]=header_data_length
    if data.len() < 9 {
        return None;
    }

    let pts_dts_flags = (data[7] >> 6) & 0x03;
    let header_data_len = data[8] as usize;
    let header_end = 9 + header_data_len;

    if header_end > data.len() {
        return None;
    }

    let mut pts = None;
    let mut dts = None;

    // The PTS (5 bytes at data[9..14]) and DTS (5 bytes at data[14..19])
    // live INSIDE the PES header, so gate on header_data_len covering them
    // (>=5 for PTS, >=10 for PTS+DTS), not merely on total length. A
    // non-conformant packet that sets the flags but declares a too-short
    // header would otherwise read payload bytes as a bogus timestamp.
    if pts_dts_flags >= 2 && header_data_len >= 5 && data.len() >= 14 {
        pts = parse_pts(&data[9..14]);
    }
    if pts_dts_flags == 3 && header_data_len >= 10 && data.len() >= 19 {
        dts = parse_pts(&data[14..19]);
    }

    let payload = &data[header_end..];

    // For private stream 1, the first payload byte is the sub-stream ID,
    // followed by a sub-header whose length depends on the sub-stream type.
    let (sub_stream_id, es_data) = if stream_id == EXTENDED_STREAM_ID {
        // HD-DVD extended-stream-id: the real stream id lives in the
        // stream_id_extension inside the PES extension. There is no leading
        // sub-header byte on the payload (unlike private_stream_1), so the ES
        // is the payload verbatim.
        (
            parse_stream_id_extension(data, data[7], header_end),
            payload.to_vec(),
        )
    } else if stream_id == PRIVATE_STREAM_1 && !payload.is_empty() {
        let sub_id = payload[0];
        let skip = match sub_id {
            0x80..=0x8F => 4, // AC3/DTS: sub_id + frame_count + access_unit_ptr(2)
            // HD-DVD Dolby Digital Plus (E-AC-3): the sub-header is the same
            // 4-byte shape as DVD AC-3 — sub_id + number_of_frames(1) +
            // first_access_unit_pointer(2). Verified empirically on ANCHORMAN
            // EVO: across every 0xC0..=0xC7 packet the 0x0B77 E-AC-3 syncword
            // sits `first_access_unit_pointer` bytes past this 4-byte header
            // (the leading bytes are the tail of the previous frame). Stripping
            // exactly these 4 bytes on EVERY packet yields a clean, continuous
            // E-AC-3 elementary stream that the ac3 parser reassembles across
            // PES boundaries; a shorter skip would splice the sub-header bytes
            // into a straddling frame and corrupt it.
            0xC0..=0xC7 => 4,
            0xA0..=0xA7 => 7, // LPCM: sub_id + frames + ptr(2) + emphasis + quant_freq + channels
            _ => 1,
        };
        let start = skip.min(payload.len());
        (Some(sub_id), payload[start..].to_vec())
    } else {
        (None, payload.to_vec())
    };

    Some(PsPacket {
        stream_id,
        sub_stream_id,
        pts,
        dts,
        data: es_data,
        // Stamped by the demuxer (extract_packets) when a source base is threaded.
        source: None,
    })
}

/// Parse a 5-byte PTS/DTS timestamp field (33 bits at 90kHz).
///
/// Layout (ISO/IEC 13818-1 Table 2-17):
/// ```text
/// byte0: [prefix:4][pts 32..30:3][marker:1]
/// byte1: [pts 29..22:8]
/// byte2: [pts 21..15:7][marker:1]
/// byte3: [pts 14..7:8]
/// byte4: [pts 6..0:7][marker:1]
/// ```
fn parse_pts(buf: &[u8]) -> Option<u64> {
    debug_assert!(buf.len() >= 5);
    // Validate the three marker bits (bit 0 of bytes 0, 2, 4) per MPEG-2
    // Systems Table 2-17. A timestamp with a cleared marker is malformed —
    // matching ts.rs::parse_timestamp, reject it rather than decode garbage.
    if (buf[0] & 0x01) == 0 || (buf[2] & 0x01) == 0 || (buf[4] & 0x01) == 0 {
        return None;
    }
    let b0 = buf[0] as u64;
    let b1 = buf[1] as u64;
    let b2 = buf[2] as u64;
    let b3 = buf[3] as u64;
    let b4 = buf[4] as u64;

    Some(((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1)
}

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

    // --- Pack header detection ---

    #[test]
    fn detect_pack_header() {
        let mut demuxer = PsDemuxer::new();

        // MPEG-2 pack header: 14 bytes, stuffing_length = 0
        let mut pack = vec![
            0x00, 0x00, 0x01, 0xBA, // start code
            0x44, 0x00, 0x04, 0x00, 0x04, 0x01, // SCR (6 bytes)
            0x01, 0x89, 0xC3, // mux_rate (3 bytes)
            0xF8, // stuffing_length = 0 (lower 3 bits)
        ];

        // Follow with a PES packet so we have a delimiter
        pack.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, // video stream
            0x00, 0x08, // length = 8
            0x80, 0x00, 0x00, // flags: no PTS/DTS, header_data_length = 0
            0xAA, 0xBB, 0xCC, 0xDD, 0xEE, // payload (5 bytes)
        ]);

        let packets = demuxer.feed(&pack);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].stream_id, 0xE0);
        assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]);
    }

    #[test]
    fn pack_header_with_stuffing() {
        let mut demuxer = PsDemuxer::new();

        // Pack header with 3 stuffing bytes
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3,
            0xFB, // stuffing_length = 3
            0xFF, 0xFF, 0xFF, // stuffing bytes
        ];

        // Followed by a PES packet
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xC0, // audio stream
            0x00, 0x05, // length = 5
            0x80, 0x00, 0x00, // flags: no PTS, header_data_len=0
            0x11, 0x22, // payload
        ]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].stream_id, 0xC0);
        assert_eq!(packets[0].data, vec![0x11, 0x22]);
    }

    // --- PES header + PTS parsing ---

    #[test]
    fn pes_header_with_pts() {
        let mut demuxer = PsDemuxer::new();

        // PTS = 90000 (1 second at 90kHz)
        // 90000 = 0x15F90
        // bit32=0, bits 29-15 = 0x0002BF, bits 14-0 = 0x1F90
        // byte0: 0010_0_1 = 0x21 ... actually let's encode properly:
        //
        // pts = 90000
        // byte0: (0010 << 4) | ((pts >> 29) & 0x0E) | 1
        //      = 0x20 | ((90000 >> 29) & 0x0E) | 1 = 0x20 | 0 | 1 = 0x21
        // byte1: (pts >> 22) & 0xFF = (90000 >> 22) & 0xFF = 0
        // byte2: ((pts >> 14) & 0xFE) | 1 = ((90000 >> 14) & 0xFE) | 1 = (0x0A & 0xFE) | 1 = 0x0B
        // byte3: (pts >> 7) & 0xFF = (90000 >> 7) & 0xFF = (703) & 0xFF = 0xBF
        // byte4: ((pts & 0x7F) << 1) | 1 = ((90000 & 0x7F) << 1) | 1 = (0x10 << 1) | 1 = 0x21

        let pts_bytes = encode_pts(90000, 0x20);

        let mut data = vec![
            0x00, 0x00, 0x01, 0xE0, // video stream
            0x00, 0x0D, // length = 13
            0x80, 0x80, 0x05, // flags: PTS only, header_data_len=5
        ];
        data.extend_from_slice(&pts_bytes);
        data.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x00]); // payload

        // Add a delimiter
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); // program end

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].stream_id, 0xE0);
        assert_eq!(packets[0].pts, Some(90000));
        assert!(packets[0].dts.is_none());
        assert_eq!(packets[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00]);
    }

    #[test]
    fn pes_header_with_pts_and_dts() {
        let mut demuxer = PsDemuxer::new();

        let pts_bytes = encode_pts(180000, 0x30); // PTS marker = 0x30
        let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10

        let mut data = vec![
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x11, // length = 17
            0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10
        ];
        data.extend_from_slice(&pts_bytes);
        data.extend_from_slice(&dts_bytes);
        data.extend_from_slice(&[0xCA, 0xFE]); // payload

        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].pts, Some(180000));
        assert_eq!(packets[0].dts, Some(90000));
    }

    // --- Private stream 1 sub-stream extraction ---

    #[test]
    fn private_stream_1_ac3_substream() {
        let mut demuxer = PsDemuxer::new();

        // AC3 sub-header: sub_id(1) + frame_count(1) + access_unit_ptr(2) = 4 bytes
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, // private stream 1
            0x00, 0x0B, // length = 11
            0x80, 0x00, 0x00, // no PTS, header_data_len=0
            0x80, // sub-stream ID: AC3 stream 0
            0x01, 0x00, 0x02, // frame_count + access_unit_ptr (sub-header bytes)
            0xAA, 0xBB, 0xCC, 0xDD, // AC3 payload
        ];

        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].stream_id, 0xBD);
        assert_eq!(packets[0].sub_stream_id, Some(0x80));
        assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
    }

    #[test]
    fn private_stream_1_dts_substream() {
        let mut demuxer = PsDemuxer::new();

        // DTS sub-header: sub_id(1) + frame_count(1) + access_unit_ptr(2) = 4 bytes
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, 0x00, 0x09, // length = 9
            0x80, 0x00, 0x00, // no PTS, header_data_len=0
            0x88, // sub-stream ID: DTS stream 0
            0x01, 0x00, 0x00, // sub-header (frame_count + access_unit_ptr)
            0x11, 0x22,
        ];
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].sub_stream_id, Some(0x88));
        assert_eq!(packets[0].data, vec![0x11, 0x22]);
    }

    #[test]
    fn private_stream_1_subtitle_substream() {
        let mut demuxer = PsDemuxer::new();

        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00,
            0x20, // sub-stream ID: subtitle stream 0
            0xFF, 0xFE,
        ];
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].sub_stream_id, Some(0x20));
    }

    #[test]
    fn private_stream_1_lpcm_substream() {
        let mut demuxer = PsDemuxer::new();

        // LPCM sub-header: sub_id(1) + frames(1) + ptr(2) + emphasis(1) + quant_freq(1) + channels(1) = 7 bytes
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, 0x00, 0x0C, // length = 12
            0x80, 0x00, 0x00, // no PTS, header_data_len=0
            0xA0, // sub-stream ID: LPCM stream 0
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // LPCM sub-header (6 bytes after sub_id)
            0x01, 0x02, // LPCM payload
        ];
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1);
        assert_eq!(packets[0].sub_stream_id, Some(0xA0));
        assert_eq!(packets[0].data, vec![0x01, 0x02]);
    }

    // --- Incremental feeding ---

    #[test]
    fn incremental_feed() {
        let mut demuxer = PsDemuxer::new();

        let mut full = vec![
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, // length = 6
            0x80, 0x00, 0x00, // no PTS, header_data_len=0
            0xAA, 0xBB, 0xCC,
        ];
        full.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        // Feed in two halves
        let mid = full.len() / 2;
        let p1 = demuxer.feed(&full[..mid]);
        assert!(p1.is_empty(), "first half should not produce packets");

        let p2 = demuxer.feed(&full[mid..]);
        assert_eq!(p2.len(), 1);
        assert_eq!(p2[0].data, vec![0xAA, 0xBB, 0xCC]);
    }

    #[test]
    fn flush_emits_trailing_unbounded_video_pes() {
        let mut demuxer = PsDemuxer::new();
        // Unbounded (length 0) video PES with no trailing start code — the
        // common EOF case. feed() must not emit it (awaiting a delimiter),
        // but flush() must emit the tail rather than discarding it.
        let data = vec![
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, // video, length 0 (unbounded)
            0x80, 0x00, 0x00, // no PTS, header_data_len = 0
            0xAA, 0xBB, 0xCC, 0xDD,
        ];
        let fed = demuxer.feed(&data);
        assert!(fed.is_empty(), "unbounded PES not emitted until delimited");
        let flushed = demuxer.flush();
        assert_eq!(flushed.len(), 1, "flush emits the trailing PES");
        assert_eq!(flushed[0].stream_id, 0xE0);
        assert_eq!(flushed[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
    }

    // --- Multiple PES packets ---

    #[test]
    fn multiple_pes_packets() {
        let mut demuxer = PsDemuxer::new();

        let mut data = Vec::new();

        // First PES: video
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22,
        ]);

        // Second PES: audio
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44,
        ]);

        // Delimiter
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 2);
        assert_eq!(packets[0].stream_id, 0xE0);
        assert_eq!(packets[1].stream_id, 0xC0);
    }

    // --- unbounded (length-0) video PES framing ---

    #[test]
    fn unbounded_video_pes_not_cut_by_embedded_start_codes() {
        // A length-0 video PES whose ES payload contains embedded MPEG start
        // codes (picture 0x00, slice 0x01, GOP 0xB8, sequence 0xB3) must be
        // delimited by the NEXT PS-layer boundary (here a program-end 0xB9),
        // not by the first embedded 00 00 01 inside the payload.
        let mut demuxer = PsDemuxer::new();

        let mut data = vec![
            0x00, 0x00, 0x01, 0xE0, // video stream
            0x00, 0x00, // length = 0 (unbounded)
            0x80, 0x00, 0x00, // flags: no PTS, header_data_len = 0
        ];
        // ES payload with embedded MPEG-2 start codes.
        let payload = [
            0x00, 0x00, 0x01, 0xB3, // sequence header
            0x11, 0x22, 0x00, 0x00, 0x01, 0x00, // picture start code
            0x33, 0x44, 0x00, 0x00, 0x01, 0x01, // slice
            0x55, 0x66,
        ];
        data.extend_from_slice(&payload);
        // PS-layer boundary that closes the unbounded PES.
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);

        let packets = demuxer.feed(&data);
        assert_eq!(packets.len(), 1, "one PES, not several payload fragments");
        assert_eq!(packets[0].stream_id, 0xE0);
        // The whole ES payload survives — none of it discarded as bogus units.
        assert_eq!(packets[0].data, payload.to_vec());
    }

    #[test]
    fn unbounded_video_pes_waits_for_boundary() {
        // Without a following PS-layer boundary the unbounded PES is held
        // (waiting for more data), not emitted truncated.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0xAA, 0xBB]); // picture SC, no PS boundary
        let packets = demuxer.feed(&data);
        assert!(packets.is_empty(), "no PS boundary yet → hold the PES");
    }

    #[test]
    fn unbounded_video_pes_buffer_is_bounded() {
        // A corrupt stream declaring an unbounded PES followed by endless
        // non-boundary bytes must not grow the buffer without limit.
        let mut demuxer = PsDemuxer::new();
        let header = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
        let packets = demuxer.feed(&header);
        assert!(packets.is_empty());
        // Feed >MAX_PS_BUFFER of bytes containing no PS-layer boundary.
        let chunk = vec![0x55u8; 1024 * 1024];
        let mut emitted = 0;
        for _ in 0..(MAX_PS_BUFFER / chunk.len() + 4) {
            emitted += demuxer.feed(&chunk).len();
        }
        assert!(
            demuxer.buffer.len() <= MAX_PS_BUFFER + chunk.len(),
            "buffer grew to {} (cap {})",
            demuxer.buffer.len(),
            MAX_PS_BUFFER
        );
        // The force-flush emits the over-long PES rather than accumulating it.
        assert!(emitted >= 1, "over-cap unbounded PES is force-flushed");
    }

    // --- PTS parsing edge cases ---

    #[test]
    fn pts_zero() {
        // PTS = 0 encoded
        let pts = parse_pts(&encode_pts(0, 0x20));
        assert_eq!(pts, Some(0));
    }

    #[test]
    fn pts_large_value() {
        // Test a large PTS value (close to 33-bit max)
        let val: u64 = (1 << 32) - 1; // 0xFFFFFFFF
        let encoded = encode_pts(val, 0x20);
        let decoded = parse_pts(&encoded);
        assert_eq!(decoded, Some(val));
    }

    // --- DVD PID mapping (track-routing collision regression) ---

    fn mk(stream_id: u8, sub: Option<u8>) -> PsPacket {
        PsPacket {
            stream_id,
            sub_stream_id: sub,
            pts: None,
            dts: None,
            data: vec![0xAA],
            source: None,
        }
    }

    #[test]
    fn parse_extended_stream_id_extracts_stream_id_extension() {
        // SHAUN's VC-1 video PES: stream_id 0xFD, flags2=0x01 (PES_extension
        // only), header_data_length=3, optional bytes 0x0F/... — build the
        // minimal well-formed variant: ext_flags=0x01 (PES_extension_flag_2),
        // field_len=0x81, stream_id_extension=0x55. Payload is the ES.
        let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID];
        let opt = [0x01u8, 0x81, 0x55];
        let es = [0xDEu8, 0xAD, 0xBE, 0xEF];
        let len = (3 + opt.len() + es.len()) as u16;
        pkt.extend_from_slice(&len.to_be_bytes());
        pkt.extend_from_slice(&[0x80, 0x01, opt.len() as u8]);
        pkt.extend_from_slice(&opt);
        pkt.extend_from_slice(&es);

        let parsed = parse_pes_packet(&pkt).expect("parses");
        assert_eq!(parsed.stream_id, EXTENDED_STREAM_ID);
        assert_eq!(
            parsed.sub_stream_id,
            Some(0x55),
            "stream_id_extension extracted from PES extension"
        );
        // ES is the payload verbatim — no leading sub-header byte stripped.
        assert_eq!(parsed.data, es);
        // Routes to the extended-stream-id PID space.
        assert_eq!(parsed.dvd_pid(), Some(hddvd_extended_pid(0x55)));
        assert_eq!(parsed.dvd_pid(), Some(0xFD55));
    }

    #[test]
    fn parse_extended_stream_id_skips_pts_and_dts_before_the_extension() {
        // The common real case: an AU-opening 0xFD VC-1 video PES carries a PTS
        // (and often DTS) in the optional-header region, which the parser must
        // SKIP (PTS +5, DTS +5) to reach the PES_extension → stream_id_extension.
        // Both branches were previously untested (flags2 there was 0x01, skipping
        // everything), so an off-by-one in the skip would silently misroute video.
        let build = |flags2: u8, skip: usize| {
            let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID];
            // optional region: `skip` bytes (PTS/DTS placeholders) then
            // ext_flags=0x01, field_len=0x81, stream_id_extension=0x55.
            let mut opt = vec![0xFFu8; skip];
            opt.extend_from_slice(&[0x01, 0x81, 0x55]);
            let es = [0xDEu8, 0xAD];
            let len = (3 + opt.len() + es.len()) as u16;
            pkt.extend_from_slice(&len.to_be_bytes());
            // flags1=0x80, flags2, header_data_length = optional region length.
            pkt.extend_from_slice(&[0x80, flags2, opt.len() as u8]);
            pkt.extend_from_slice(&opt);
            pkt.extend_from_slice(&es);
            pkt
        };
        // PTS present (pts_dts bits = 10 → flags2 0x80) + PES_extension (0x01).
        let pts_only = parse_pes_packet(&build(0x81, 5)).expect("parses");
        assert_eq!(
            pts_only.sub_stream_id,
            Some(0x55),
            "extension found after skipping a 5-byte PTS"
        );
        // PTS+DTS present (pts_dts bits = 11 → flags2 0xC0) + PES_extension.
        let pts_dts = parse_pes_packet(&build(0xC1, 10)).expect("parses");
        assert_eq!(
            pts_dts.sub_stream_id,
            Some(0x55),
            "extension found after skipping a 10-byte PTS+DTS"
        );
    }

    #[test]
    fn parse_extended_stream_id_without_extension_yields_no_sub_id() {
        // A 0xFD PES that declares no PES_extension (flags2=0x00) can't carry a
        // stream_id_extension → sub_stream_id None, and dvd_pid falls through.
        let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID];
        let es = [0x11u8, 0x22];
        let len = (3 + es.len()) as u16;
        pkt.extend_from_slice(&len.to_be_bytes());
        pkt.extend_from_slice(&[0x80, 0x00, 0x00]);
        pkt.extend_from_slice(&es);

        let parsed = parse_pes_packet(&pkt).expect("parses");
        assert_eq!(parsed.sub_stream_id, None);
        assert_eq!(parsed.dvd_pid(), None);
        assert_eq!(parsed.data, es);
    }

    #[test]
    fn dvd_pid_matches_scanner_assignment() {
        // Video → 0xE0 (matches dvd.rs VideoStream pid).
        assert_eq!(mk(0xE0, None).dvd_pid(), Some(DVD_VIDEO_PID));
        // PID = 0xBD00 | sub_stream_id — unique per sub-id, no collision.
        assert_eq!(mk(0xBD, Some(0x80)).dvd_pid(), Some(0xBD80)); // AC-3 #0
        assert_eq!(mk(0xBD, Some(0x81)).dvd_pid(), Some(0xBD81)); // AC-3 #1
        assert_eq!(mk(0xBD, Some(0x88)).dvd_pid(), Some(0xBD88)); // DTS  #0
        assert_eq!(mk(0xBD, Some(0xA0)).dvd_pid(), Some(0xBDA0)); // LPCM #0
        // VobSub subtitle 0x20/0x21 → 0x20 / 0x21 (identity).
        assert_eq!(mk(0xBD, Some(0x20)).dvd_pid(), Some(0x20));
        assert_eq!(mk(0xBD, Some(0x21)).dvd_pid(), Some(0x21));
        // Unmappable: MPEG audio, private stream 2, bogus sub-id.
        assert_eq!(mk(0xC0, None).dvd_pid(), None);
        assert_eq!(mk(0xBF, None).dvd_pid(), None);
        assert_eq!(mk(0xBD, Some(0x10)).dvd_pid(), None);
    }

    #[test]
    fn mixed_codec_audio_does_not_collide() {
        // The core regression: a title mixing AC-3 (0x80), DTS (0x88) and
        // LPCM (0xA0) audio. The old per-codec relative arithmetic mapped
        // all three to 0xBD00. They must now get distinct PIDs that match
        // what dvd.rs assigns from the same dvd_audio_pid() table.
        let ac3 = mk(0xBD, Some(0x80)).dvd_pid().unwrap();
        let dts = mk(0xBD, Some(0x88)).dvd_pid().unwrap();
        let lpcm = mk(0xBD, Some(0xA0)).dvd_pid().unwrap();
        assert_ne!(ac3, dts, "AC-3 and DTS must not collide");
        assert_ne!(ac3, lpcm, "AC-3 and LPCM must not collide");
        assert_ne!(dts, lpcm, "DTS and LPCM must not collide");

        // Scanner side uses the same table; build a pid_to_track for a
        // mixed-codec title [video, AC-3, DTS, LPCM, sub] and route every
        // PS packet to its own distinct track.
        let pid_to_track: Vec<(u16, usize)> = vec![
            (DVD_VIDEO_PID, 0),
            (dvd_audio_pid(0x80).unwrap(), 1),
            (dvd_audio_pid(0x88).unwrap(), 2),
            (dvd_audio_pid(0xA0).unwrap(), 3),
            (dvd_subtitle_pid(0x20).unwrap(), 4),
        ];
        let route = |p: PsPacket| -> Option<usize> {
            let pid = p.dvd_pid()?;
            pid_to_track
                .iter()
                .find(|(x, _)| *x == pid)
                .map(|(_, t)| *t)
        };
        assert_eq!(route(mk(0xE0, None)), Some(0));
        assert_eq!(route(mk(0xBD, Some(0x80))), Some(1)); // AC-3 → its own track
        assert_eq!(route(mk(0xBD, Some(0x88))), Some(2)); // DTS  → its own track
        assert_eq!(route(mk(0xBD, Some(0xA0))), Some(3)); // LPCM → its own track
        assert_eq!(route(mk(0xBD, Some(0x20))), Some(4)); // sub  → its own track
    }

    #[test]
    fn subtitle_does_not_collide_with_audio_track() {
        // Subtitle sub-id 0x20 routes to its own subtitle PID (0x20),
        // distinct from any audio PID (0xBD80+).
        let audio0 = mk(0xBD, Some(0x80)).dvd_pid().unwrap(); // 0xBD80
        let sub0 = mk(0xBD, Some(0x20)).dvd_pid().unwrap(); // 0x20
        assert_ne!(
            audio0, sub0,
            "subtitle sub-id 0x20 must NOT map to the audio PID"
        );

        let pid_to_track: Vec<(u16, usize)> = vec![
            (DVD_VIDEO_PID, 0),
            (dvd_audio_pid(0x80).unwrap(), 1),
            (dvd_audio_pid(0x81).unwrap(), 2),
            (dvd_subtitle_pid(0x20).unwrap(), 3),
            (dvd_subtitle_pid(0x21).unwrap(), 4),
        ];
        let route = |p: PsPacket| -> Option<usize> {
            let pid = p.dvd_pid()?;
            pid_to_track
                .iter()
                .find(|(x, _)| *x == pid)
                .map(|(_, t)| *t)
        };
        assert_eq!(route(mk(0xE0, None)), Some(0));
        assert_eq!(route(mk(0xBD, Some(0x80))), Some(1));
        assert_eq!(route(mk(0xBD, Some(0x81))), Some(2));
        assert_eq!(route(mk(0xBD, Some(0x20))), Some(3)); // sub0 → track 3, NOT 1
        assert_eq!(route(mk(0xBD, Some(0x21))), Some(4)); // sub1 → track 4, NOT 2
    }

    // --- Helper: encode PTS for tests ---

    fn encode_pts(pts: u64, marker_prefix: u8) -> [u8; 5] {
        let mut buf = [0u8; 5];
        buf[0] = marker_prefix | (((pts >> 30) as u8) & 0x07) << 1 | 1;
        buf[1] = ((pts >> 22) & 0xFF) as u8;
        buf[2] = (((pts >> 15) & 0x7F) as u8) << 1 | 1;
        buf[3] = ((pts >> 7) & 0xFF) as u8;
        buf[4] = (((pts) & 0x7F) as u8) << 1 | 1;
        buf
    }

    // ════════════════════════════════════════════════════════════════════
    // Added hardening tests
    // ════════════════════════════════════════════════════════════════════

    /// Program-end start code (00 00 01 B9) — used as a delimiter so a
    /// bounded or unbounded PES preceding it is fully framed.
    const PROGRAM_END: [u8; 4] = [0x00, 0x00, 0x01, 0xB9];

    // ── parse_pts: full 33-bit field round trip (ISO 13818-1 Table 2-17) ──

    #[test]
    fn parse_pts_max_33bit() {
        // The PTS field is exactly 33 bits; 2^33-1 must round-trip — a
        // truncated shift/mask would lose the top bits.
        let max = (1u64 << 33) - 1;
        assert_eq!(parse_pts(&encode_pts(max, 0x20)), Some(max));
    }

    #[test]
    fn parse_pts_rejects_bad_marker_bits() {
        // A timestamp with any marker bit (bit 0 of bytes 0/2/4) cleared is
        // malformed and must be rejected, matching ts.rs::parse_timestamp.
        let mut buf = encode_pts(90000, 0x20);
        assert!(parse_pts(&buf).is_some());
        buf[0] &= !0x01;
        assert_eq!(parse_pts(&buf), None);
        let mut buf = encode_pts(90000, 0x20);
        buf[2] &= !0x01;
        assert_eq!(parse_pts(&buf), None);
        let mut buf = encode_pts(90000, 0x20);
        buf[4] &= !0x01;
        assert_eq!(parse_pts(&buf), None);
    }

    // ── pack header (0xBA) framing ────────────────────────────────────────

    #[test]
    fn pack_header_waits_for_full_14_bytes() {
        // A pack header needs 14 bytes (MPEG-2). A buffer with only the
        // start code + a few bytes must NOT advance past it — the demuxer
        // waits for more data rather than misframing.
        let mut demuxer = PsDemuxer::new();
        // 00 00 01 BA then only 6 of the 10 remaining pack bytes.
        let partial = vec![0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01];
        let p = demuxer.feed(&partial);
        assert!(p.is_empty());
        // Now supply the rest of the pack (stuffing=0) plus a PES + delimiter.
        let mut rest = vec![0x01, 0x89, 0xC3, 0xF8]; // mux_rate(3) + stuffing byte
        rest.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0xAB, 0xCD,
        ]);
        rest.extend_from_slice(&PROGRAM_END);
        let p2 = demuxer.feed(&rest);
        assert_eq!(p2.len(), 1, "PES after a now-complete pack header parses");
        assert_eq!(p2[0].data, vec![0xAB, 0xCD]);
    }

    #[test]
    fn pack_header_stuffing_length_consumed() {
        // pack_stuffing_length = low 3 bits of byte 13 (ISO 13818-1
        // §2.5.3.4). The demuxer must skip exactly 14 + stuffing bytes. The
        // stuffing region here holds a DECOY PES start code (00 00 01 E0…);
        // if the stuffing count is under-consumed the scanner would re-sync
        // onto that decoy and emit a bogus PES. Correct skip lands directly
        // on the REAL PES.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3,
            0xFD, // stuffing_length = 5 (low 3 bits of 0xFD = 0b101)
            // 5 stuffing bytes containing a decoy PES start code.
            0x00, 0x00, 0x01, 0xE0, 0xDE,
        ];
        // Real PES carries 0x11 0x22; the decoy (if mis-parsed) would carry
        // garbage with a different/short payload.
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22,
        ]);
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1, "exactly the real PES; the decoy was skipped");
        assert_eq!(p[0].data, vec![0x11, 0x22]);
    }

    // ── system header (0xBB) framing ──────────────────────────────────────

    #[test]
    fn system_header_length_skipped() {
        // System header: 00 00 01 BB [header_length:2] body. The demuxer
        // must skip 6 + header_length bytes (ISO 13818-1 §2.5.3.5), even
        // though the body contains bytes that look like PES IDs.
        let mut demuxer = PsDemuxer::new();
        let body = [0x00, 0x00, 0x01, 0xE0, 0xFF, 0xFF]; // decoy PES-looking bytes
        let mut data = vec![0x00, 0x00, 0x01, 0xBB];
        data.extend_from_slice(&(body.len() as u16).to_be_bytes());
        data.extend_from_slice(&body);
        // Real PES after the system header.
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44,
        ]);
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(
            p.len(),
            1,
            "decoy bytes inside system header not parsed as PES"
        );
        assert_eq!(p[0].stream_id, 0xC0);
        assert_eq!(p[0].data, vec![0x33, 0x44]);
    }

    #[test]
    fn system_header_waits_for_full_body() {
        // System header declaring a body longer than buffered must not
        // advance — wait for more data.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xBB, 0x00, 0x20]; // len=32
        data.extend_from_slice(&[0xAA; 4]); // only 4 of 32 body bytes
        assert!(demuxer.feed(&data).is_empty());
    }

    // ── PES length / boundary handling ────────────────────────────────────

    #[test]
    fn bounded_pes_waits_for_full_declared_length() {
        // A PES with a non-zero PES_packet_length must not be emitted until
        // all 6 + length bytes are buffered — never emit a short frame.
        let mut demuxer = PsDemuxer::new();
        // length = 5 → total 11 bytes, supply only 9.
        let head = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00];
        assert!(demuxer.feed(&head).is_empty());
        // supply the remaining 2 payload bytes.
        let p = demuxer.feed(&[0xEE, 0xFF]);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].data, vec![0xEE, 0xFF]);
    }

    #[test]
    fn padding_stream_0xbe_is_dropped() {
        // Padding stream (0xBE) carries no ES (ISO 13818-1 Table 2-22) and
        // must produce no PsPacket — only the real PES survives.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xBE, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF];
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x01, 0x02,
        ]);
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1, "padding stream dropped; only real PES emitted");
        assert_eq!(p[0].stream_id, 0xE0);
    }

    #[test]
    fn private_stream_2_0xbf_has_no_pes_extension() {
        // private_stream_2 (0xBF) carries no standard PES header extension
        // (ISO 13818-1 Table 2-22): the bytes after the 6-byte prefix are
        // raw payload, NOT flags/header_data_length. No PTS, no sub-stream.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xBF, 0x00, 0x04, 0xDE, 0xAD, 0xBE, 0xEF];
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].stream_id, 0xBF);
        assert_eq!(p[0].pts, None, "0xBF carries no PTS");
        assert_eq!(p[0].sub_stream_id, None);
        assert_eq!(p[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
    }

    #[test]
    fn unknown_start_code_is_skipped_not_parsed() {
        // A start code with an ID outside the known PS-layer set
        // (e.g. 0xB0, reserved) must be skipped 4 bytes and not derail
        // the following real PES.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xB0]; // unknown/reserved code
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x9A, 0xBC,
        ]);
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].data, vec![0x9A, 0xBC]);
    }

    // ── private_stream_1 sub-header skip lengths ──────────────────────────

    #[test]
    fn private_stream_1_unknown_subid_skips_one_byte() {
        // For a private_stream_1 sub-id outside the AC3/DTS/LPCM ranges the
        // skip is 1 (just the sub-id byte). All remaining bytes are ES.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00, //
            0x70, // sub-id outside known ranges → skip 1
            0x55, 0x66,
        ];
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].sub_stream_id, Some(0x70));
        assert_eq!(p[0].data, vec![0x55, 0x66], "only sub-id byte skipped");
    }

    #[test]
    fn private_stream_1_short_payload_does_not_underflow_skip() {
        // If the sub-header skip exceeds the payload length, `skip.min(len)`
        // clamps so ES is empty rather than panicking on an out-of-range
        // slice. AC3 skip is 4 but only 2 payload bytes present.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, 0x00, 0x04, 0x80, 0x00, 0x00, //
            0x80, // AC3 sub-id, skip=4
            0x01, // only 1 byte after sub-id (total payload 2 < skip 4)
        ];
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].sub_stream_id, Some(0x80));
        assert!(
            p[0].data.is_empty(),
            "clamped skip yields empty ES, no panic"
        );
    }

    // ── dvd_audio_pid / dvd_subtitle_pid range boundaries ─────────────────

    #[test]
    fn dvd_audio_pid_range_boundaries() {
        // AC3/DTS audio sub-ids 0x80..=0x8F and LPCM 0xA0..=0xA7 map to
        // 0xBD00|sub. Just-outside values must return None.
        assert_eq!(dvd_audio_pid(0x80), Some(0xBD80));
        assert_eq!(dvd_audio_pid(0x8F), Some(0xBD8F));
        assert_eq!(dvd_audio_pid(0xA0), Some(0xBDA0));
        assert_eq!(dvd_audio_pid(0xA7), Some(0xBDA7));
        // Boundaries just outside the ranges.
        assert_eq!(dvd_audio_pid(0x7F), None);
        assert_eq!(dvd_audio_pid(0x90), None);
        assert_eq!(dvd_audio_pid(0x9F), None);
        assert_eq!(dvd_audio_pid(0xA8), None);
    }

    #[test]
    fn hddvd_ddplus_substream_maps_to_bd_pid() {
        // HD-DVD Dolby Digital Plus sub-ids 0xC0..=0xC7 map to 0xBD00|sub,
        // distinct per track and disjoint from the DVD audio space. A DVD never
        // emits these, so the range is purely additive.
        assert_eq!(dvd_audio_pid(0xC0), Some(0xBDC0));
        assert_eq!(dvd_audio_pid(0xC3), Some(0xBDC3));
        assert_eq!(dvd_audio_pid(0xC7), Some(0xBDC7));
        // Just outside the range.
        assert_eq!(dvd_audio_pid(0xBF), None);
        assert_eq!(dvd_audio_pid(0xC8), None);
        // Four DD+ tracks (ANCHORMAN) get four distinct PIDs.
        let pids: Vec<u16> = (0xC0u8..=0xC3).map(|s| dvd_audio_pid(s).unwrap()).collect();
        assert_eq!(pids, vec![0xBDC0, 0xBDC1, 0xBDC2, 0xBDC3]);
        // And route through dvd_pid on a private_stream_1 packet.
        assert_eq!(mk(0xBD, Some(0xC0)).dvd_pid(), Some(0xBDC0));
        assert_eq!(mk(0xBD, Some(0xC3)).dvd_pid(), Some(0xBDC3));
    }

    #[test]
    fn hddvd_ddplus_pes_strips_4byte_subheader_to_syncword() {
        // A private_stream_1 PES carrying DD+ (sub-id 0xC0) has a 4-byte
        // sub-header (sub_id + num_frames(1) + access_unit_ptr(2)); the demuxer
        // must strip exactly those 4 bytes so es_data begins at the E-AC-3
        // payload — here the 0x0B77 syncword sits right after the sub-header.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![
            0x00, 0x00, 0x01, 0xBD, // private stream 1
            0x00, 0x0B, // PES_packet_length = 11 (flags2 + hdl1 + 8 payload)
            0x80, 0x00, 0x00, // no PTS, header_data_len = 0
            0xC0, // sub-stream id: DD+ track 0
            0x01, 0x00, 0x00, // num_frames(1) + access_unit_ptr(2)
            0x0B, 0x77, 0xDE, 0xAD, // E-AC-3 syncword + payload
        ];
        data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].sub_stream_id, Some(0xC0));
        assert_eq!(
            p[0].data,
            vec![0x0B, 0x77, 0xDE, 0xAD],
            "4-byte DD+ sub-header stripped; es_data starts at the syncword"
        );
    }

    #[test]
    fn dvd_subtitle_pid_range_boundaries() {
        // VobSub subtitle sub-ids 0x20..=0x3F map to the identity PID.
        assert_eq!(dvd_subtitle_pid(0x20), Some(0x20));
        assert_eq!(dvd_subtitle_pid(0x3F), Some(0x3F));
        assert_eq!(dvd_subtitle_pid(0x1F), None);
        assert_eq!(dvd_subtitle_pid(0x40), None);
    }

    #[test]
    fn dvd_pid_all_video_stream_ids_map_to_video() {
        // ISO 13818-1: 0xE0..=0xEF are all video streams. DVD collapses
        // them onto the single canonical video PID.
        for sid in 0xE0u8..=0xEF {
            assert_eq!(
                mk(sid, None).dvd_pid(),
                Some(DVD_VIDEO_PID),
                "stream_id {sid:#04x} must map to video"
            );
        }
    }

    // ── flushing semantics ────────────────────────────────────────────────

    #[test]
    fn flush_discards_incomplete_bounded_pes() {
        // A bounded PES short of its declared length is genuinely incomplete
        // and must be DROPPED at flush — not emitted with a truncated payload.
        let mut demuxer = PsDemuxer::new();
        // length=10 but only 2 payload bytes supplied.
        let head = vec![
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x00, 0x00, 0xAA, 0xBB,
        ];
        assert!(demuxer.feed(&head).is_empty());
        let flushed = demuxer.flush();
        assert!(
            flushed.is_empty(),
            "incomplete bounded PES must not be emitted on flush"
        );
    }

    #[test]
    fn pes_header_data_length_skips_pts_when_flag_unset() {
        // If pts_dts_flags == 0 the 5 "PTS" bytes after the fixed header are
        // ES, not a timestamp. A PES with header_data_length=0 and no PTS
        // flag must surface no PTS and keep all payload bytes.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![
            0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, 0x80, 0x00, 0x00, 0x21, 0x00, 0x01,
        ];
        // 0x21 0x00 0x01 look like the start of a PTS field but must NOT be
        // parsed as one (flags2 = 0x00 ⇒ no PTS).
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 1);
        assert_eq!(p[0].pts, None);
        assert_eq!(p[0].data, vec![0x21, 0x00, 0x01]);
    }

    #[test]
    fn unbounded_video_pes_framed_by_next_pes_not_embedded_audio_code() {
        // An unbounded (length 0) video PES must be delimited by the next
        // PS-layer unit. A following AUDIO PES (0xC0) is a valid boundary,
        // so the video ES must include its embedded 00 00 01 00 picture
        // code but stop at the audio PES start.
        let mut demuxer = PsDemuxer::new();
        let mut data = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
        let video_payload = [0x11, 0x00, 0x00, 0x01, 0x00, 0x22]; // embedded picture SC
        data.extend_from_slice(&video_payload);
        // Next PS-layer unit: an audio PES (bounded).
        data.extend_from_slice(&[
            0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x99, 0x88,
        ]);
        data.extend_from_slice(&PROGRAM_END);
        let p = demuxer.feed(&data);
        assert_eq!(p.len(), 2, "video PES + audio PES");
        assert_eq!(p[0].stream_id, 0xE0);
        assert_eq!(
            p[0].data, video_payload,
            "video ES keeps its embedded start code, stops at the audio PES"
        );
        assert_eq!(p[1].stream_id, 0xC0);
        assert_eq!(p[1].data, vec![0x99, 0x88]);
    }
}
