//! DreamStack Bitstream WASM Codec
//!
//! WebAssembly bindings for the bitstream binary protocol.
//! Provides encode/decode for headers, RLE compression, and delta frames.
//!
//! This eliminates protocol drift between JS and Rust implementations
//! by sharing the exact same codec logic in the browser.

use wasm_bindgen::prelude::*;

// ─── Protocol Constants ───

pub const HEADER_SIZE: usize = 16;

// Frame types
pub const FRAME_PIXELS: u8 = 0x01;
pub const FRAME_COMPRESSED: u8 = 0x02;
pub const FRAME_DELTA: u8 = 0x03;
pub const FRAME_AUDIO_PCM: u8 = 0x10;
pub const FRAME_AUDIO_COMPRESSED: u8 = 0x11;
pub const FRAME_HAPTIC: u8 = 0x20;
pub const FRAME_ACTUATOR: u8 = 0x21;
pub const FRAME_LED_MATRIX: u8 = 0x22;
pub const FRAME_SIGNAL_SYNC: u8 = 0x30;
pub const FRAME_SIGNAL_DIFF: u8 = 0x31;
pub const FRAME_SCHEMA_ANNOUNCE: u8 = 0x32;
pub const FRAME_SUBSCRIBE_FILTER: u8 = 0x33;
pub const FRAME_NEURAL_FRAME: u8 = 0x40;
pub const FRAME_NEURAL_AUDIO: u8 = 0x41;
pub const FRAME_NEURAL_ACTUATOR: u8 = 0x42;
pub const FRAME_NEURAL_LATENT: u8 = 0x43;
pub const FRAME_KEYFRAME: u8 = 0xF0;
pub const FRAME_ACK: u8 = 0xFD;
pub const FRAME_PING: u8 = 0xFE;
pub const FRAME_END: u8 = 0xFF;

// Flags
pub const FLAG_INPUT: u8 = 0x01;
pub const FLAG_KEYFRAME: u8 = 0x02;
pub const FLAG_COMPRESSED: u8 = 0x04;

// Input types
pub const INPUT_POINTER: u8 = 0x01;
pub const INPUT_PTR_DOWN: u8 = 0x02;
pub const INPUT_PTR_UP: u8 = 0x03;
pub const INPUT_KEY_DOWN: u8 = 0x10;
pub const INPUT_KEY_UP: u8 = 0x11;
pub const INPUT_TOUCH: u8 = 0x20;
pub const INPUT_TOUCH_END: u8 = 0x21;
pub const INPUT_GAMEPAD_AXIS: u8 = 0x30;
pub const INPUT_GAMEPAD_BUTTON: u8 = 0x31;
pub const INPUT_MIDI: u8 = 0x40;
pub const INPUT_SCROLL: u8 = 0x50;
pub const INPUT_RESIZE: u8 = 0x60;
pub const INPUT_VOICE: u8 = 0x70;
pub const INPUT_CAMERA: u8 = 0x71;
pub const INPUT_SENSOR: u8 = 0x80;
pub const INPUT_BCI: u8 = 0x90;

// ─── Header Encode/Decode ───

/// Encode a 16-byte frame header.
#[wasm_bindgen]
pub fn encode_header(
    frame_type: u8,
    flags: u8,
    seq: u16,
    timestamp: u32,
    width: u16,
    height: u16,
    payload_len: u32,
) -> Vec<u8> {
    let mut buf = vec![0u8; HEADER_SIZE];
    buf[0] = frame_type;
    buf[1] = flags;
    buf[2..4].copy_from_slice(&seq.to_le_bytes());
    buf[4..8].copy_from_slice(&timestamp.to_le_bytes());
    buf[8..10].copy_from_slice(&width.to_le_bytes());
    buf[10..12].copy_from_slice(&height.to_le_bytes());
    buf[12..16].copy_from_slice(&payload_len.to_le_bytes());
    buf
}

/// Decode header fields from a buffer. Returns [type, flags, seq, timestamp, width, height, length].
#[wasm_bindgen]
pub fn decode_header(buf: &[u8]) -> Vec<u32> {
    if buf.len() < HEADER_SIZE {
        return vec![];
    }
    vec![
        buf[0] as u32,                                                    // type
        buf[1] as u32,                                                    // flags
        u16::from_le_bytes([buf[2], buf[3]]) as u32,                     // seq
        u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),            // timestamp
        u16::from_le_bytes([buf[8], buf[9]]) as u32,                     // width
        u16::from_le_bytes([buf[10], buf[11]]) as u32,                   // height
        u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),        // length
    ]
}

// ─── RLE Compression ───

/// RLE-encode a delta buffer for transmission.
///
/// Encoding: non-zero bytes pass through; runs of zero bytes become
/// `0x00 count_lo count_hi` (2-byte LE count, max 65535 per run).
///
/// Achieves 5-20x compression on typical XOR delta frames (70-95% zeros).
#[wasm_bindgen]
pub fn rle_encode(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() / 2);
    let mut i = 0;
    while i < data.len() {
        if data[i] == 0 {
            let start = i;
            while i < data.len() && data[i] == 0 {
                i += 1;
            }
            let count = i - start;
            let mut remaining = count;
            while remaining > 0 {
                let chunk = remaining.min(65535);
                out.push(0x00);
                out.push((chunk & 0xFF) as u8);
                out.push(((chunk >> 8) & 0xFF) as u8);
                remaining -= chunk;
            }
        } else {
            out.push(data[i]);
            i += 1;
        }
    }
    out
}

/// RLE-decode a compressed delta buffer.
///
/// Inverse of `rle_encode`: expands `0x00 count_lo count_hi` into zero runs.
#[wasm_bindgen]
pub fn rle_decode(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() * 2);
    let mut i = 0;
    while i < data.len() {
        if data[i] == 0 {
            if i + 2 >= data.len() {
                break;
            }
            let count = data[i + 1] as usize | ((data[i + 2] as usize) << 8);
            out.resize(out.len() + count, 0);
            i += 3;
        } else {
            out.push(data[i]);
            i += 1;
        }
    }
    out
}

// ─── Delta Compression ───

/// Compute XOR delta between two equal-length frames.
#[wasm_bindgen]
pub fn compute_delta(current: &[u8], previous: &[u8]) -> Vec<u8> {
    current.iter().zip(previous.iter()).map(|(c, p)| c ^ p).collect()
}

/// Apply XOR delta to reconstruct the current frame.
#[wasm_bindgen]
pub fn apply_delta(previous: &[u8], delta: &[u8]) -> Vec<u8> {
    previous.iter().zip(delta.iter()).map(|(p, d)| p ^ d).collect()
}

/// Encode a delta frame: XOR + RLE. Returns compressed bytes.
#[wasm_bindgen]
pub fn encode_delta_rle(current: &[u8], previous: &[u8]) -> Vec<u8> {
    let delta = compute_delta(current, previous);
    rle_encode(&delta)
}

/// Decode a delta frame: RLE decompress + XOR apply.
#[wasm_bindgen]
pub fn decode_delta_rle(compressed: &[u8], previous: &[u8]) -> Vec<u8> {
    let delta = rle_decode(compressed);
    apply_delta(previous, &delta)
}

// ─── Message Builders ───

/// Build a complete message: header + payload.
#[wasm_bindgen]
pub fn build_message(
    frame_type: u8,
    flags: u8,
    seq: u16,
    timestamp: u32,
    width: u16,
    height: u16,
    payload: &[u8],
) -> Vec<u8> {
    let header = encode_header(frame_type, flags, seq, timestamp, width, height, payload.len() as u32);
    let mut msg = Vec::with_capacity(HEADER_SIZE + payload.len());
    msg.extend_from_slice(&header);
    msg.extend_from_slice(payload);
    msg
}

/// Build a signal diff message from JSON bytes.
#[wasm_bindgen]
pub fn signal_diff_message(seq: u16, timestamp: u32, json: &[u8]) -> Vec<u8> {
    build_message(FRAME_SIGNAL_DIFF, 0, seq, timestamp, 0, 0, json)
}

/// Build a signal sync (keyframe) message from JSON bytes.
#[wasm_bindgen]
pub fn signal_sync_message(seq: u16, timestamp: u32, json: &[u8]) -> Vec<u8> {
    build_message(FRAME_SIGNAL_SYNC, FLAG_KEYFRAME, seq, timestamp, 0, 0, json)
}

/// Build an input message with FLAG_INPUT set.
#[wasm_bindgen]
pub fn input_message(input_type: u8, seq: u16, timestamp: u32, payload: &[u8]) -> Vec<u8> {
    build_message(input_type, FLAG_INPUT, seq, timestamp, 0, 0, payload)
}

// ─── Neural Frame Builders ───

/// Build a neural frame message (model output tensor).
#[wasm_bindgen]
pub fn neural_frame_message(seq: u16, timestamp: u32, width: u16, height: u16, tensor_data: &[u8]) -> Vec<u8> {
    build_message(FRAME_NEURAL_FRAME, 0, seq, timestamp, width, height, tensor_data)
}

/// Build a neural audio synthesis output message.
#[wasm_bindgen]
pub fn neural_audio_message(seq: u16, timestamp: u32, audio_data: &[u8]) -> Vec<u8> {
    build_message(FRAME_NEURAL_AUDIO, 0, seq, timestamp, 0, 0, audio_data)
}

/// Build a neural actuator command message.
#[wasm_bindgen]
pub fn neural_actuator_message(seq: u16, timestamp: u32, command_data: &[u8]) -> Vec<u8> {
    build_message(FRAME_NEURAL_ACTUATOR, 0, seq, timestamp, 0, 0, command_data)
}

/// Build a neural latent space representation message.
#[wasm_bindgen]
pub fn neural_latent_message(seq: u16, timestamp: u32, latent_data: &[u8]) -> Vec<u8> {
    build_message(FRAME_NEURAL_LATENT, 0, seq, timestamp, 0, 0, latent_data)
}

// ─── Audio Frame Builder ───

/// Build an Opus-compressed audio frame message.
#[wasm_bindgen]
pub fn audio_compressed_message(seq: u16, timestamp: u32, channels: u16, sample_rate_div100: u16, opus_data: &[u8]) -> Vec<u8> {
    build_message(FRAME_AUDIO_COMPRESSED, FLAG_COMPRESSED, seq, timestamp, channels, sample_rate_div100, opus_data)
}

// ─── MIDI Input Builder ───

/// Build a MIDI input message: status(u8), data1(u8), data2(u8).
#[wasm_bindgen]
pub fn midi_input_message(seq: u16, timestamp: u32, status: u8, data1: u8, data2: u8) -> Vec<u8> {
    build_message(INPUT_MIDI, FLAG_INPUT, seq, timestamp, 0, 0, &[status, data1, data2])
}

/// Build a sensor input message: sensor_type(u8), x(i16), y(i16), z(i16).
#[wasm_bindgen]
pub fn sensor_input_message(seq: u16, timestamp: u32, sensor_type: u8, x: i16, y: i16, z: i16) -> Vec<u8> {
    let mut payload = vec![sensor_type];
    payload.extend_from_slice(&x.to_le_bytes());
    payload.extend_from_slice(&y.to_le_bytes());
    payload.extend_from_slice(&z.to_le_bytes());
    build_message(INPUT_SENSOR, FLAG_INPUT, seq, timestamp, 0, 0, &payload)
}

// ─── ACK Builder ───

/// Build an ACK message: ack_seq(u16), rtt_ms(u16).
#[wasm_bindgen]
pub fn ack_message(seq: u16, timestamp: u32, ack_seq: u16, rtt_ms: u16) -> Vec<u8> {
    let mut payload = Vec::with_capacity(4);
    payload.extend_from_slice(&ack_seq.to_le_bytes());
    payload.extend_from_slice(&rtt_ms.to_le_bytes());
    build_message(FRAME_ACK, 0, seq, timestamp, 0, 0, &payload)
}

// ─── Stream Parsing ───

/// Parse a buffer containing one or more concatenated messages.
/// Returns a flat array of [header_offset, payload_offset, payload_len, ...] triples.
/// Each triple represents one complete message in the buffer.
#[wasm_bindgen]
pub fn parse_stream(buf: &[u8]) -> Vec<u32> {
    let mut result = Vec::new();
    let mut offset = 0;
    while offset + HEADER_SIZE <= buf.len() {
        let payload_len = u32::from_le_bytes([
            buf[offset + 12],
            buf[offset + 13],
            buf[offset + 14],
            buf[offset + 15],
        ]) as usize;
        let total = HEADER_SIZE + payload_len;
        if offset + total > buf.len() {
            break;
        }
        result.push(offset as u32);
        result.push((offset + HEADER_SIZE) as u32);
        result.push(payload_len as u32);
        offset += total;
    }
    result
}

// ─── Debug / Introspection Helpers ───

/// Get a human-readable name for a frame type byte.
/// When frame type and input type share the same byte value (e.g., 0x31),
/// the frame type name takes priority. Use with FLAG_INPUT to disambiguate.
#[wasm_bindgen]
pub fn frame_type_name(type_byte: u8) -> String {
    // Frame types (prioritized) — unique values only
    let name = match type_byte {
        0x01 => "Pixels",
        0x02 => "CompressedPixels",
        0x03 => "DeltaPixels",
        0x10 => "AudioPcm",
        0x11 => "AudioCompressed",
        0x20 => "Haptic",
        0x21 => "Actuator",
        0x22 => "LedMatrix",
        0x30 => "SignalSync",
        0x31 => "SignalDiff",
        0x32 => "SchemaAnnounce",
        0x33 => "SubscribeFilter",
        0x40 => "NeuralFrame",
        0x41 => "NeuralAudio",
        0x42 => "NeuralActuator",
        0x43 => "NeuralLatent",
        0x50 => "Scroll",
        0x60 => "Resize",
        0x70 => "VoiceInput",
        0x71 => "CameraInput",
        0x80 => "SensorInput",
        0x90 => "BciInput",
        0xF0 => "Keyframe",
        0xFD => "Ack",
        0xFE => "Ping",
        0xFF => "End",
        _ => "Unknown",
    };
    name.into()
}

/// Get the frame type name considering the FLAG_INPUT flag.
/// When the input flag is set, resolves shared byte values to input names.
#[wasm_bindgen]
pub fn frame_type_name_with_flag(type_byte: u8, flags: u8) -> String {
    if flags & FLAG_INPUT != 0 {
        let name = match type_byte {
            0x01 => "Pointer",
            0x02 => "PointerDown",
            0x03 => "PointerUp",
            0x10 => "KeyDown",
            0x11 => "KeyUp",
            0x20 => "Touch",
            0x21 => "TouchEnd",
            0x30 => "GamepadAxis",
            0x31 => "GamepadButton",
            0x40 => "Midi",
            0x50 => "Scroll",
            0x60 => "Resize",
            0x70 => "VoiceInput",
            0x71 => "CameraInput",
            0x80 => "SensorInput",
            0x90 => "BciInput",
            _ => "Unknown",
        };
        return name.into();
    }
    frame_type_name(type_byte)
}

/// Check if a frame type byte could represent an input event.
/// Note: some values overlap with frame types (e.g., 0x01 = Pixels or Pointer).
/// Use FLAG_INPUT to disambiguate.
#[wasm_bindgen]
pub fn is_input_frame(type_byte: u8) -> bool {
    matches!(type_byte,
        0x01 | 0x02 | 0x03 |  // Pointer, PointerDown, PointerUp
        0x10 | 0x11 |          // KeyDown, KeyUp
        0x20 | 0x21 |          // Touch, TouchEnd
        0x30 | 0x31 |          // GamepadAxis, GamepadButton
        0x40 |                 // Midi
        0x50 | 0x60 |          // Scroll, Resize
        0x70 | 0x71 |          // VoiceInput, CameraInput
        0x80 | 0x90            // SensorInput, BciInput
    )
}

/// Compute compression stats. Returns [ratio, savings_percent].
#[wasm_bindgen]
pub fn compression_stats(original_len: u32, compressed_len: u32) -> Vec<f64> {
    if original_len == 0 {
        return vec![1.0, 0.0];
    }
    let ratio = compressed_len as f64 / original_len as f64;
    let savings = (1.0 - ratio) * 100.0;
    vec![ratio, savings]
}

// ─── v0.3: TypedFrameHeader ───

/// Compressed pixel format constants
pub const COMPRESSED_FMT_PNG: u8 = 0;
pub const COMPRESSED_FMT_WEBP: u8 = 1;
pub const COMPRESSED_FMT_JPEG: u8 = 2;

/// DSREC recording magic bytes
pub const DSREC_MAGIC: [u8; 3] = [0x44, 0x53, 0x01];

/// A structured frame header with JS-friendly getter methods.
#[wasm_bindgen]
pub struct TypedFrameHeader {
    frame_type: u8,
    flags: u8,
    seq: u16,
    timestamp: u32,
    width: u16,
    height: u16,
    payload_length: u32,
}

#[wasm_bindgen]
impl TypedFrameHeader {
    pub fn frame_type(&self) -> u8 { self.frame_type }
    pub fn flags(&self) -> u8 { self.flags }
    pub fn seq(&self) -> u16 { self.seq }
    pub fn timestamp(&self) -> u32 { self.timestamp }
    pub fn width(&self) -> u16 { self.width }
    pub fn height(&self) -> u16 { self.height }
    pub fn payload_length(&self) -> u32 { self.payload_length }
    pub fn is_input(&self) -> bool { self.flags & FLAG_INPUT != 0 }
    pub fn is_keyframe(&self) -> bool { self.flags & FLAG_KEYFRAME != 0 }
    pub fn is_compressed(&self) -> bool { self.flags & FLAG_COMPRESSED != 0 }
    pub fn type_name(&self) -> String { frame_type_name(self.frame_type) }
    pub fn type_name_resolved(&self) -> String { frame_type_name_with_flag(self.frame_type, self.flags) }
}

/// Decode a buffer into a TypedFrameHeader struct (JS-friendly object).
#[wasm_bindgen]
pub fn decode_header_typed(buf: &[u8]) -> Option<TypedFrameHeader> {
    if buf.len() < HEADER_SIZE { return None; }
    Some(TypedFrameHeader {
        frame_type: buf[0],
        flags: buf[1],
        seq: u16::from_le_bytes([buf[2], buf[3]]),
        timestamp: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
        width: u16::from_le_bytes([buf[8], buf[9]]),
        height: u16::from_le_bytes([buf[10], buf[11]]),
        payload_length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
    })
}

// ─── v0.3: Recording Decoder ───

/// Decode one recording entry from a .dsrec file buffer at given offset.
/// Returns [timestamp_hi, timestamp_lo, msg_offset, msg_len] or empty if invalid.
#[wasm_bindgen]
pub fn decode_recording_entry(buf: &[u8], offset: usize) -> Vec<u32> {
    if offset + 12 > buf.len() { return Vec::new(); }
    let ts = u64::from_le_bytes([
        buf[offset], buf[offset+1], buf[offset+2], buf[offset+3],
        buf[offset+4], buf[offset+5], buf[offset+6], buf[offset+7],
    ]);
    let msg_len = u32::from_le_bytes([
        buf[offset+8], buf[offset+9], buf[offset+10], buf[offset+11],
    ]);
    let msg_offset = offset + 12;
    if msg_offset + msg_len as usize > buf.len() { return Vec::new(); }
    vec![(ts >> 32) as u32, ts as u32, msg_offset as u32, msg_len]
}

// ─── v0.3: Compressed Pixel Builder ───

/// Build a CompressedPixels message with format byte.
#[wasm_bindgen]
pub fn compressed_pixel_message(
    seq: u16, timestamp: u32, width: u16, height: u16,
    format: u8, image_data: &[u8],
) -> Vec<u8> {
    let mut payload = Vec::with_capacity(1 + image_data.len());
    payload.push(format);
    payload.extend_from_slice(image_data);
    build_message(FRAME_COMPRESSED, FLAG_COMPRESSED, seq, timestamp, width, height, &payload)
}

// ─── v0.4: Session Recorder ───

/// In-memory recording session. Accumulates messages, exports as .dsrec bytes.
#[wasm_bindgen]
pub struct SessionRecorder {
    entries: Vec<(u64, Vec<u8>)>, // (timestamp_ms, message)
}

#[wasm_bindgen]
impl SessionRecorder {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self { entries: Vec::new() }
    }

    /// Record a message with millisecond timestamp.
    pub fn record(&mut self, timestamp_ms: u64, msg: &[u8]) {
        self.entries.push((timestamp_ms, msg.to_vec()));
    }

    /// Export as .dsrec format bytes.
    pub fn export(&self) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&DSREC_MAGIC);
        for (ts, msg) in &self.entries {
            // Convert ms to us for .dsrec format
            let ts_us = ts * 1000;
            buf.extend_from_slice(&ts_us.to_le_bytes());
            buf.extend_from_slice(&(msg.len() as u32).to_le_bytes());
            buf.extend_from_slice(msg);
        }
        buf
    }

    pub fn entry_count(&self) -> u32 {
        self.entries.len() as u32
    }

    pub fn duration_ms(&self) -> u64 {
        if self.entries.is_empty() { return 0; }
        let first = self.entries.first().unwrap().0;
        let last = self.entries.last().unwrap().0;
        last - first
    }
}

/// In-memory .dsrec player. Load buffer, iterate entries.
#[wasm_bindgen]
pub struct SessionPlayer {
    entries: Vec<(u64, usize, usize)>, // (timestamp_us, msg_offset, msg_len)
    buf: Vec<u8>,
    cursor: usize,
}

#[wasm_bindgen]
impl SessionPlayer {
    /// Load a .dsrec buffer. Returns None if magic is invalid.
    pub fn load(buf: &[u8]) -> Option<SessionPlayer> {
        if buf.len() < 3 || buf[0..3] != DSREC_MAGIC {
            return None;
        }
        let mut entries = Vec::new();
        let mut offset = 3; // skip magic
        while offset + 12 <= buf.len() {
            let ts = u64::from_le_bytes([
                buf[offset], buf[offset+1], buf[offset+2], buf[offset+3],
                buf[offset+4], buf[offset+5], buf[offset+6], buf[offset+7],
            ]);
            let msg_len = u32::from_le_bytes([
                buf[offset+8], buf[offset+9], buf[offset+10], buf[offset+11],
            ]) as usize;
            let msg_offset = offset + 12;
            if msg_offset + msg_len > buf.len() { break; }
            entries.push((ts, msg_offset, msg_len));
            offset = msg_offset + msg_len;
        }
        Some(SessionPlayer { entries, buf: buf.to_vec(), cursor: 0 })
    }

    /// Get next entry: [ts_hi, ts_lo, msg_offset, msg_len]. Empty if done.
    pub fn next_entry(&mut self) -> Vec<u32> {
        if self.cursor >= self.entries.len() { return Vec::new(); }
        let (ts, offset, len) = self.entries[self.cursor];
        self.cursor += 1;
        vec![(ts >> 32) as u32, ts as u32, offset as u32, len as u32]
    }

    /// Seek to first entry at or after timestamp (microseconds).
    pub fn seek_to_time(&mut self, timestamp_us: u64) {
        self.cursor = self.entries.iter()
            .position(|(ts, _, _)| *ts >= timestamp_us)
            .unwrap_or(self.entries.len());
    }

    pub fn total_entries(&self) -> u32 {
        self.entries.len() as u32
    }

    pub fn duration_ms(&self) -> u64 {
        if self.entries.is_empty() { return 0; }
        let first = self.entries.first().unwrap().0;
        let last = self.entries.last().unwrap().0;
        (last - first) / 1000 // us to ms
    }

    /// Get raw message bytes at an offset (for JS to decode).
    pub fn get_message(&self, offset: usize, len: usize) -> Vec<u8> {
        if offset + len > self.buf.len() { return Vec::new(); }
        self.buf[offset..offset+len].to_vec()
    }
}

// ─── v0.4: Quality Helpers ───

/// Determine quality tier from RTT. 0=Full (<50ms), 1=Reduced (50-150ms), 2=Minimal (>150ms).
#[wasm_bindgen]
pub fn adaptive_quality_tier(rtt_ms: u16) -> u8 {
    if rtt_ms < 50 { 0 }
    else if rtt_ms < 150 { 1 }
    else { 2 }
}

/// Calculate bandwidth in kilobits per second.
#[wasm_bindgen]
pub fn bandwidth_kbps(bytes: u32, elapsed_ms: u32) -> f64 {
    if elapsed_ms == 0 { return 0.0; }
    (bytes as f64 * 8.0) / (elapsed_ms as f64 / 1000.0) / 1000.0
}

// ─── v0.5: Auth Constants ───

pub const FRAME_AUTH: u8 = 0x0F;

/// Build an auth challenge message (server → client).
#[wasm_bindgen]
pub fn auth_challenge_message(seq: u16, nonce: &[u8]) -> Vec<u8> {
    let mut payload = Vec::with_capacity(9);
    payload.push(0u8); // phase 0 = challenge
    if nonce.len() >= 8 {
        payload.extend_from_slice(&nonce[..8]);
    } else {
        payload.extend_from_slice(nonce);
        payload.resize(9, 0);
    }
    build_message(FRAME_AUTH, 0, seq, 0, 0, 0, &payload)
}

/// Build an auth response message (client → server).
#[wasm_bindgen]
pub fn auth_response_message(seq: u16, nonce: &[u8], token: &[u8]) -> Vec<u8> {
    let mut payload = Vec::with_capacity(9 + token.len());
    payload.push(1u8); // phase 1 = response
    if nonce.len() >= 8 {
        payload.extend_from_slice(&nonce[..8]);
    } else {
        payload.extend_from_slice(nonce);
        payload.resize(9, 0);
    }
    payload.extend_from_slice(token);
    build_message(FRAME_AUTH, 0, seq, 0, 0, 0, &payload)
}

// ─── v0.5: XOR Scrambling ───

/// XOR-scramble payload bytes with a repeating key.
#[wasm_bindgen]
pub fn scramble(payload: &[u8], key: &[u8]) -> Vec<u8> {
    if key.is_empty() { return payload.to_vec(); }
    payload.iter().enumerate().map(|(i, &b)| b ^ key[i % key.len()]).collect()
}

/// Descramble (same as scramble — XOR is its own inverse).
#[wasm_bindgen]
pub fn descramble(payload: &[u8], key: &[u8]) -> Vec<u8> {
    scramble(payload, key)
}

// ─── v0.5: Recording Metadata ───

/// Decode recording metadata (24 bytes after .dsrec magic).
/// Returns [version, created_at_hi, created_at_lo, duration_ms, frame_count, width, height].
#[wasm_bindgen]
pub fn decode_recording_metadata(buf: &[u8]) -> Vec<u32> {
    if buf.len() < 24 { return Vec::new(); }
    let version = u16::from_le_bytes([buf[0], buf[1]]) as u32;
    let created_at = u64::from_le_bytes([buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9]]);
    let duration_ms = u32::from_le_bytes([buf[10], buf[11], buf[12], buf[13]]);
    let frame_count = u32::from_le_bytes([buf[14], buf[15], buf[16], buf[17]]);
    let width = u16::from_le_bytes([buf[18], buf[19]]) as u32;
    let height = u16::from_le_bytes([buf[20], buf[21]]) as u32;
    vec![version, (created_at >> 32) as u32, created_at as u32, duration_ms, frame_count, width, height]
}

// ─── v0.10: Stream Multiplexing ───

pub const STREAM_ID_MASK: u8 = 0xF0;
pub const FLAGS_MASK: u8 = 0x0F;

/// Extract stream ID from flags byte (upper 4 bits).
#[wasm_bindgen]
pub fn extract_stream_id(flags: u8) -> u8 {
    (flags & STREAM_ID_MASK) >> 4
}

/// Embed stream ID into flags byte (preserves lower 4 flag bits).
#[wasm_bindgen]
pub fn embed_stream_id(flags: u8, stream_id: u8) -> u8 {
    (flags & FLAGS_MASK) | ((stream_id & 0x0F) << 4)
}

/// Multiplex: embed stream_id into a frame's flags byte.
#[wasm_bindgen]
pub fn mux_message(stream_id: u8, msg: &[u8]) -> Vec<u8> {
    if msg.len() < HEADER_SIZE { return msg.to_vec(); }
    let mut out = msg.to_vec();
    out[1] = embed_stream_id(out[1], stream_id);
    out
}

/// Demultiplex: extract stream_id and strip it. Returns [stream_id, ...cleaned_msg].
#[wasm_bindgen]
pub fn demux_message(msg: &[u8]) -> Vec<u8> {
    if msg.len() < HEADER_SIZE { return msg.to_vec(); }
    let stream_id = extract_stream_id(msg[1]);
    let mut out = Vec::with_capacity(1 + msg.len());
    out.push(stream_id);
    out.extend_from_slice(msg);
    // Clear stream bits in the copied flags byte
    out[2] &= FLAGS_MASK; // offset by 1 because of prepended stream_id
    out
}

/// Get priority for a frame type (0=Critical, 1=High, 2=Normal, 3=Low).
#[wasm_bindgen]
pub fn frame_priority(frame_type: u8) -> u8 {
    match frame_type {
        0xF0 | 0x0F | 0xFF => 0, // Keyframe, Auth, End = Critical
        0x01 | 0x02 | 0x03 | 0x40 => 1, // Pixels variants, NeuralFrame = High
        0x30 | 0x31 | 0x10 | 0x11 => 2, // Signals, Audio = Normal
        0xFE | 0xFD => 3, // Ping, Ack = Low
        _ => 2, // Default Normal
    }
}

/// Should a frame be dropped given priority and congestion level (0-3)?
#[wasm_bindgen]
pub fn should_drop_frame(priority: u8, congestion: u8) -> bool {
    match congestion {
        0 => false,
        1 => priority >= 3, // Drop Low
        2 => priority >= 2, // Drop Normal+Low
        _ => priority >= 1, // Drop everything except Critical
    }
}

// ─── v0.11: Frame Stats & Latency ───

use std::cell::RefCell;

thread_local! {
    static STATS_COUNT: RefCell<u64> = RefCell::new(0);
    static STATS_BYTES: RefCell<u64> = RefCell::new(0);
    static STATS_DROPPED: RefCell<u64> = RefCell::new(0);
    static RTT_SAMPLES: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}

/// Record a frame stat.
#[wasm_bindgen]
pub fn record_frame_stat(len: u32, dropped: bool) {
    if dropped {
        STATS_DROPPED.with(|d| *d.borrow_mut() += 1);
    } else {
        STATS_COUNT.with(|c| *c.borrow_mut() += 1);
        STATS_BYTES.with(|b| *b.borrow_mut() += len as u64);
    }
}

/// Get frame stats as [count, bytes_hi, bytes_lo, dropped, avg_size].
#[wasm_bindgen]
pub fn get_frame_stats() -> Vec<u32> {
    let count = STATS_COUNT.with(|c| *c.borrow());
    let bytes = STATS_BYTES.with(|b| *b.borrow());
    let dropped = STATS_DROPPED.with(|d| *d.borrow());
    let avg = if count > 0 { (bytes / count) as u32 } else { 0 };
    vec![count as u32, (bytes >> 32) as u32, bytes as u32, dropped as u32, avg]
}

/// Record an RTT sample in milliseconds.
#[wasm_bindgen]
pub fn record_rtt(ms: f64) {
    RTT_SAMPLES.with(|s| {
        let mut samples = s.borrow_mut();
        if samples.len() >= 100 { samples.remove(0); }
        samples.push(ms);
    });
}

/// Get average RTT in ms.
#[wasm_bindgen]
pub fn get_avg_rtt() -> f64 {
    RTT_SAMPLES.with(|s| {
        let samples = s.borrow();
        if samples.is_empty() { return 0.0; }
        samples.iter().sum::<f64>() / samples.len() as f64
    })
}

/// Get jitter (RTT standard deviation) in ms.
#[wasm_bindgen]
pub fn get_jitter() -> f64 {
    RTT_SAMPLES.with(|s| {
        let samples = s.borrow();
        if samples.len() < 2 { return 0.0; }
        let avg = samples.iter().sum::<f64>() / samples.len() as f64;
        let var = samples.iter().map(|s| (s - avg).powi(2)).sum::<f64>() / samples.len() as f64;
        var.sqrt()
    })
}

/// Composite stream health (0.0 - 1.0).
#[wasm_bindgen]
pub fn stream_health_score() -> f32 {
    let count = STATS_COUNT.with(|c| *c.borrow());
    let bytes = STATS_BYTES.with(|b| *b.borrow());
    let dropped = STATS_DROPPED.with(|d| *d.borrow());
    let avg_rtt = get_avg_rtt();
    let total = count + dropped;
    let drop_rate = if total > 0 { dropped as f64 / total as f64 } else { 0.0 };
    let throughput = bytes as f64; // approximate

    let frame_score = (count as f64 / 30.0).min(1.0);
    let latency_score = (1.0 - (avg_rtt / 500.0).min(1.0)).max(0.0);
    let throughput_score = (throughput / 1_000_000.0).min(1.0);
    let drop_score = (1.0 - drop_rate * 5.0).max(0.0);
    (frame_score * 0.2 + latency_score * 0.3 + throughput_score * 0.2 + drop_score * 0.3) as f32
}


// ─── Tests ───

// ─── v0.12: State Snapshot ───

/// Encode state snapshot: [version:u32][data...].
#[wasm_bindgen]
pub fn encode_state_snapshot(version: u32, data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(4 + data.len());
    out.extend_from_slice(&version.to_le_bytes());
    out.extend_from_slice(data);
    out
}

/// Decode state snapshot. Returns [version, ...data] or empty if too short.
#[wasm_bindgen]
pub fn decode_state_snapshot(msg: &[u8]) -> Vec<u8> {
    if msg.len() < 4 { return vec![]; }
    let version = u32::from_le_bytes([msg[0], msg[1], msg[2], msg[3]]);
    let mut result = Vec::with_capacity(4 + msg.len() - 4);
    result.extend_from_slice(&version.to_le_bytes());
    result.extend_from_slice(&msg[4..]);
    result
}

// ─── v0.12: Gap Detector ───

thread_local! {
    static NEXT_EXPECTED_SEQ: RefCell<u16> = RefCell::new(0);
    static SEEN_SEQS: RefCell<Vec<u16>> = RefCell::new(Vec::new());
}

/// Feed a sequence number. Returns: 0=ok, 1=gap, 2=duplicate.
#[wasm_bindgen]
pub fn feed_seq(seq: u16) -> u8 {
    SEEN_SEQS.with(|seen| {
        let mut seen = seen.borrow_mut();
        if seen.contains(&seq) { return 2; } // duplicate
        if seen.len() >= 256 { seen.drain(..128); }
        seen.push(seq);
        NEXT_EXPECTED_SEQ.with(|next| {
            let mut expected = next.borrow_mut();
            if seq == *expected {
                *expected = expected.wrapping_add(1);
                0 // ok
            } else {
                *expected = seq.wrapping_add(1);
                1 // gap
            }
        })
    })
}

// ─── v0.12: Protocol Version ───

/// Current protocol version.
#[wasm_bindgen]
pub fn protocol_version() -> u16 { 12 }

/// Check if a remote version is compatible.
#[wasm_bindgen]
pub fn check_protocol_version(v: u16) -> bool { v == 12 }

// ─── v0.13: Replay Ring Buffer ───

thread_local! {
    static REPLAY_BUFFER: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static REPLAY_MAX: RefCell<usize> = RefCell::new(300);
    static CODEC_NAMES: RefCell<Vec<String>> = RefCell::new(Vec::new());
    static CODEC_IDS: RefCell<Vec<u8>> = RefCell::new(Vec::new());
}

/// Push a frame into the replay ring buffer.
#[wasm_bindgen]
pub fn push_replay(frame: &[u8]) {
    REPLAY_BUFFER.with(|buf| {
        let mut buf = buf.borrow_mut();
        let max = REPLAY_MAX.with(|m| *m.borrow());
        if buf.len() >= max { buf.remove(0); }
        buf.push(frame.to_vec());
    });
}

/// Get replay frames in range [start, end). Returns concatenated with 4-byte length prefixes.
#[wasm_bindgen]
pub fn get_replay_range(start: usize, end: usize) -> Vec<u8> {
    REPLAY_BUFFER.with(|buf| {
        let buf = buf.borrow();
        let end = end.min(buf.len());
        let mut out = Vec::new();
        for i in start..end {
            let frame = &buf[i];
            out.extend_from_slice(&(frame.len() as u32).to_le_bytes());
            out.extend_from_slice(frame);
        }
        out
    })
}

/// Get replay buffer length.
#[wasm_bindgen]
pub fn replay_len() -> usize {
    REPLAY_BUFFER.with(|buf| buf.borrow().len())
}

// ─── v0.13: Header Serde ───

/// Serialize a frame header to 16 bytes.
#[wasm_bindgen]
pub fn serialize_frame_header(frame_type: u8, flags: u8, seq: u16, timestamp: u32, width: u16, height: u16, payload_len: u32) -> Vec<u8> {
    let mut out = Vec::with_capacity(16);
    out.push(frame_type);
    out.push(flags);
    out.extend_from_slice(&seq.to_le_bytes());
    out.extend_from_slice(&timestamp.to_le_bytes());
    out.extend_from_slice(&width.to_le_bytes());
    out.extend_from_slice(&height.to_le_bytes());
    out.extend_from_slice(&payload_len.to_le_bytes());
    out
}

/// Deserialize a 16-byte header. Returns [type, flags, seq, timestamp, width, height, len].
#[wasm_bindgen]
pub fn deserialize_frame_header(data: &[u8]) -> Vec<u32> {
    if data.len() < 16 { return vec![]; }
    vec![
        data[0] as u32,
        data[1] as u32,
        u16::from_le_bytes([data[2], data[3]]) as u32,
        u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
        u16::from_le_bytes([data[8], data[9]]) as u32,
        u16::from_le_bytes([data[10], data[11]]) as u32,
        u32::from_le_bytes([data[12], data[13], data[14], data[15]]),
    ]
}

// ─── v0.13: Codec Registry ───

/// Register a named codec.
#[wasm_bindgen]
pub fn register_codec(name: &str, id: u8) {
    CODEC_NAMES.with(|n| n.borrow_mut().push(name.to_string()));
    CODEC_IDS.with(|i| i.borrow_mut().push(id));
}

/// List registered codecs as comma-separated string.
#[wasm_bindgen]
pub fn list_codecs() -> String {
    CODEC_NAMES.with(|n| n.borrow().join(","))
}

// ─── v0.14: RLE Compression ───

/// RLE compress: [byte, count, byte, count, ...].
#[wasm_bindgen]
pub fn rle_compress(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    if data.is_empty() { return out; }
    let mut i = 0;
    while i < data.len() {
        let val = data[i];
        let mut count: u8 = 1;
        while i + (count as usize) < data.len() && data[i + count as usize] == val && count < 255 {
            count += 1;
        }
        out.push(val);
        out.push(count);
        i += count as usize;
    }
    out
}

/// RLE decompress.
#[wasm_bindgen]
pub fn rle_decompress(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    let mut i = 0;
    while i + 1 < data.len() {
        let val = data[i];
        let count = data[i + 1] as usize;
        for _ in 0..count { out.push(val); }
        i += 2;
    }
    out
}

// ─── v0.14: Client Sync Drift ───

thread_local! {
    static CLIENT_SEQ: RefCell<u16> = RefCell::new(0);
    static CLIENT_ACK: RefCell<u16> = RefCell::new(0);
}

/// Set current client sequence number.
#[wasm_bindgen]
pub fn set_client_seq(seq: u16) { CLIENT_SEQ.with(|s| *s.borrow_mut() = seq); }

/// Set last acknowledged sequence.
#[wasm_bindgen]
pub fn set_client_ack(ack: u16) { CLIENT_ACK.with(|a| *a.borrow_mut() = ack); }

/// Get sync drift (seq - ack).
#[wasm_bindgen]
pub fn get_sync_drift() -> i32 {
    CLIENT_SEQ.with(|s| CLIENT_ACK.with(|a| *s.borrow() as i32 - *a.borrow() as i32))
}

// ─── v0.14: Bandwidth Limiting ───

thread_local! {
    static BW_LIMIT: RefCell<usize> = RefCell::new(0);
    static BW_CONSUMED: RefCell<usize> = RefCell::new(0);
}

/// Set bandwidth limit (bytes per second). 0 = unlimited.
#[wasm_bindgen]
pub fn set_bandwidth_limit(bps: usize) { BW_LIMIT.with(|l| *l.borrow_mut() = bps); }

/// Check if frame of given size can be sent. Returns 0=yes, 1=no.
#[wasm_bindgen]
pub fn check_bandwidth(size: usize) -> u8 {
    BW_LIMIT.with(|limit| {
        let limit = *limit.borrow();
        if limit == 0 { return 0; } // unlimited
        BW_CONSUMED.with(|consumed| {
            let mut consumed = consumed.borrow_mut();
            if *consumed + size <= limit {
                *consumed += size;
                0
            } else {
                1
            }
        })
    })
}

/// Reset bandwidth counter (call once per second).
#[wasm_bindgen]
pub fn reset_bandwidth() { BW_CONSUMED.with(|c| *c.borrow_mut() = 0); }

// ─── v0.15: Adaptive Quality ───

thread_local! {
    static ABR_RTT: RefCell<Vec<f32>> = RefCell::new(Vec::new());
    static ABR_LOSS: RefCell<Vec<f32>> = RefCell::new(Vec::new());
    static TRANSFORM_NAMES: RefCell<Vec<String>> = RefCell::new(Vec::new());
}

/// Feed a quality sample (RTT in ms, loss in %).
#[wasm_bindgen]
pub fn feed_quality_sample(rtt: f32, loss: f32) {
    ABR_RTT.with(|s| {
        let mut s = s.borrow_mut();
        if s.len() >= 30 { s.remove(0); }
        s.push(rtt);
    });
    ABR_LOSS.with(|s| {
        let mut s = s.borrow_mut();
        if s.len() >= 30 { s.remove(0); }
        s.push(loss);
    });
}

/// Get recommended quality tier 0-4.
#[wasm_bindgen]
pub fn recommended_quality() -> u8 {
    let avg_rtt = ABR_RTT.with(|s| {
        let s = s.borrow();
        if s.is_empty() { 0.0 } else { s.iter().sum::<f32>() / s.len() as f32 }
    });
    let avg_loss = ABR_LOSS.with(|s| {
        let s = s.borrow();
        if s.is_empty() { 0.0 } else { s.iter().sum::<f32>() / s.len() as f32 }
    });
    if avg_rtt > 200.0 || avg_loss > 10.0 { 0 }
    else if avg_rtt > 100.0 || avg_loss > 5.0 { 1 }
    else if avg_rtt > 50.0 || avg_loss > 2.0 { 2 }
    else if avg_rtt > 20.0 || avg_loss > 0.5 { 3 }
    else { 4 }
}

// ─── v0.15: Metrics Snapshot ───

/// Get a JSON snapshot of current stream metrics.
#[wasm_bindgen]
pub fn metrics_snapshot(fps: f32, bitrate: u32, clients: u32, drift: f32, health: f32) -> String {
    format!(
        r#"{{"fps":{:.1},"bitrate":{},"clients":{},"drift":{:.1},"health":{:.2},"quality":{}}}"#,
        fps, bitrate, clients, drift, health, recommended_quality(),
    )
}

// ─── v0.15: Frame Transforms ───

/// Add a named transform stage.
#[wasm_bindgen]
pub fn add_frame_transform(name: &str) {
    TRANSFORM_NAMES.with(|n| n.borrow_mut().push(name.to_string()));
}

/// Apply transforms (identity pass-through, returns same data). Returns stage count in first byte.
#[wasm_bindgen]
pub fn apply_transforms(data: &[u8]) -> Vec<u8> {
    let count = TRANSFORM_NAMES.with(|n| n.borrow().len()) as u8;
    let mut out = Vec::with_capacity(1 + data.len());
    out.push(count);
    out.extend_from_slice(data);
    out
}

/// List transform stages as comma-separated.
#[wasm_bindgen]
pub fn list_transforms() -> String {
    TRANSFORM_NAMES.with(|n| n.borrow().join(","))
}

// ─── v0.16: Frame Encryption ───

/// XOR encrypt frame data with rotating key.
#[wasm_bindgen]
pub fn encrypt_frame(data: &[u8], key: &[u8]) -> Vec<u8> {
    if key.is_empty() { return data.to_vec(); }
    data.iter().enumerate().map(|(i, &b)| b ^ key[i % key.len()]).collect()
}

/// XOR decrypt frame data (symmetric with encrypt).
#[wasm_bindgen]
pub fn decrypt_frame(data: &[u8], key: &[u8]) -> Vec<u8> {
    encrypt_frame(data, key)
}

// ─── v0.16: Migration ───

thread_local! {
    static MIGRATION_STATE: RefCell<Vec<u8>> = RefCell::new(Vec::new());
    static DEDUP_HASHES: RefCell<Vec<u64>> = RefCell::new(Vec::new());
}

/// Prepare migration state (serialize seq + client count).
#[wasm_bindgen]
pub fn prepare_migration(seq: u16, clients: u16) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&seq.to_le_bytes());
    buf.extend_from_slice(&clients.to_le_bytes());
    MIGRATION_STATE.with(|s| *s.borrow_mut() = buf.clone());
    buf
}

/// Accept migration state. Returns [seq, clients] or empty on error.
#[wasm_bindgen]
pub fn accept_migration(state: &[u8]) -> Vec<u16> {
    if state.len() < 4 { return vec![]; }
    let seq = u16::from_le_bytes([state[0], state[1]]);
    let clients = u16::from_le_bytes([state[2], state[3]]);
    MIGRATION_STATE.with(|s| *s.borrow_mut() = state.to_vec());
    vec![seq, clients]
}

// ─── v0.16: Frame Dedup ───

fn fnv_hash(data: &[u8]) -> u64 {
    let mut h: u64 = 0xcbf29ce484222325;
    for &b in data {
        h ^= b as u64;
        h = h.wrapping_mul(0x100000001b3);
    }
    h
}

/// Check if frame is duplicate. Returns 1=dup, 0=new.
#[wasm_bindgen]
pub fn is_frame_duplicate(data: &[u8]) -> u8 {
    let h = fnv_hash(data);
    DEDUP_HASHES.with(|hashes| {
        let mut hashes = hashes.borrow_mut();
        if hashes.contains(&h) { return 1; }
        if hashes.len() >= 200 { hashes.remove(0); }
        hashes.push(h);
        0
    })
}

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

    #[test]
    fn test_header_roundtrip() {
        let encoded = encode_header(0x31, 0x02, 42, 1000, 800, 600, 256);
        let decoded = decode_header(&encoded);
        assert_eq!(decoded, vec![0x31, 0x02, 42, 1000, 800, 600, 256]);
    }

    #[test]
    fn test_rle_roundtrip() {
        let data = vec![0, 0, 0, 0, 0, 0x42, 0x43, 0, 0, 0];
        let encoded = rle_encode(&data);
        let decoded = rle_decode(&encoded);
        assert_eq!(decoded, data);
    }

    #[test]
    fn test_rle_compression_ratio() {
        let data = vec![0u8; 1000];
        let encoded = rle_encode(&data);
        assert_eq!(encoded.len(), 3);
        let decoded = rle_decode(&encoded);
        assert_eq!(decoded.len(), 1000);
    }

    #[test]
    fn test_delta_roundtrip() {
        let prev = vec![10, 20, 30, 40];
        let curr = vec![10, 25, 30, 45];
        let delta = compute_delta(&curr, &prev);
        let reconstructed = apply_delta(&prev, &delta);
        assert_eq!(reconstructed, curr);
    }

    #[test]
    fn test_delta_rle_roundtrip() {
        let prev = vec![0u8; 100];
        let mut curr = vec![0u8; 100];
        curr[50] = 0xFF;
        curr[51] = 0xAB;
        let compressed = encode_delta_rle(&curr, &prev);
        assert!(compressed.len() < 100);
        let reconstructed = decode_delta_rle(&compressed, &prev);
        assert_eq!(reconstructed, curr);
    }

    #[test]
    fn test_message_builder() {
        let msg = signal_diff_message(1, 500, b"{\"count\":42}");
        assert!(msg.len() == HEADER_SIZE + 12);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_SIGNAL_DIFF as u32);
        assert_eq!(header[6], 12);
    }

    #[test]
    fn test_input_message() {
        let payload = vec![100u8, 0, 200, 0, 1];
        let msg = input_message(INPUT_POINTER, 0, 0, &payload);
        let header = decode_header(&msg);
        assert_eq!(header[0], INPUT_POINTER as u32);
        assert_eq!(header[1], FLAG_INPUT as u32);
    }

    #[test]
    fn test_neural_frame_message() {
        let tensor = vec![0u8; 64];
        let msg = neural_frame_message(1, 100, 4, 4, &tensor);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_NEURAL_FRAME as u32);
        assert_eq!(header[4], 4);
        assert_eq!(header[5], 4);
        assert_eq!(header[6], 64);
    }

    #[test]
    fn test_audio_compressed_message() {
        let opus = vec![0xAA; 128];
        let msg = audio_compressed_message(1, 200, 2, 480, &opus);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_AUDIO_COMPRESSED as u32);
        assert_eq!(header[1], FLAG_COMPRESSED as u32);
        assert_eq!(header[4], 2);
        assert_eq!(header[5], 480);
    }

    #[test]
    fn test_midi_input_message() {
        let msg = midi_input_message(1, 100, 0x90, 60, 100);
        let header = decode_header(&msg);
        assert_eq!(header[0], INPUT_MIDI as u32);
        assert_eq!(header[1], FLAG_INPUT as u32);
        assert_eq!(msg[HEADER_SIZE], 0x90);
        assert_eq!(msg[HEADER_SIZE + 1], 60);
        assert_eq!(msg[HEADER_SIZE + 2], 100);
    }

    #[test]
    fn test_sensor_input_message() {
        let msg = sensor_input_message(1, 100, 0, 100, -200, 9800);
        let header = decode_header(&msg);
        assert_eq!(header[0], INPUT_SENSOR as u32);
        assert_eq!(header[1], FLAG_INPUT as u32);
        assert_eq!(msg[HEADER_SIZE], 0);
    }

    // ─── v0.2 Tests ───

    #[test]
    fn test_ack_message() {
        let msg = ack_message(5, 1000, 42, 15);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_ACK as u32);
        assert_eq!(header[6], 4); // payload = 2 + 2 bytes
        // Check payload
        let ack_seq = u16::from_le_bytes([msg[HEADER_SIZE], msg[HEADER_SIZE + 1]]);
        let rtt = u16::from_le_bytes([msg[HEADER_SIZE + 2], msg[HEADER_SIZE + 3]]);
        assert_eq!(ack_seq, 42);
        assert_eq!(rtt, 15);
    }

    #[test]
    fn test_parse_stream_single() {
        let msg = signal_diff_message(1, 100, b"{\"x\":1}");
        let offsets = parse_stream(&msg);
        assert_eq!(offsets.len(), 3);
        assert_eq!(offsets[0], 0);            // header_offset
        assert_eq!(offsets[1], HEADER_SIZE as u32); // payload_offset
        assert_eq!(offsets[2], 7);            // payload_len
    }

    #[test]
    fn test_parse_stream_multi() {
        let msg1 = signal_diff_message(1, 100, b"{\"a\":1}");
        let msg2 = signal_sync_message(2, 200, b"{\"b\":2}");
        let mut combined = Vec::new();
        combined.extend_from_slice(&msg1);
        combined.extend_from_slice(&msg2);

        let offsets = parse_stream(&combined);
        assert_eq!(offsets.len(), 6); // 2 messages × 3 values each
        assert_eq!(offsets[0], 0);
        assert_eq!(offsets[3], msg1.len() as u32);
    }

    #[test]
    fn test_parse_stream_partial() {
        let msg = signal_diff_message(1, 100, b"{\"x\":1}");
        // Truncate the message — should return 0 messages
        let partial = &msg[..msg.len() - 2];
        let offsets = parse_stream(partial);
        assert_eq!(offsets.len(), 0);
    }

    #[test]
    fn test_frame_type_name() {
        assert_eq!(frame_type_name(FRAME_PIXELS), "Pixels");
        assert_eq!(frame_type_name(FRAME_ACK), "Ack");
        assert_eq!(frame_type_name(FRAME_PING), "Ping");
        assert_eq!(frame_type_name(INPUT_SCROLL), "Scroll");
        assert_eq!(frame_type_name(0x99), "Unknown");
    }

    #[test]
    fn test_is_input_frame() {
        assert!(is_input_frame(INPUT_POINTER));
        assert!(is_input_frame(INPUT_SCROLL));
        assert!(is_input_frame(INPUT_BCI));
        // FRAME_PIXELS=0x01=INPUT_POINTER — shared value, returns true
        assert!(is_input_frame(FRAME_PIXELS));
        // Unique frame types that are NOT input types
        assert!(!is_input_frame(FRAME_PING));
        assert!(!is_input_frame(FRAME_ACK));
        assert!(!is_input_frame(FRAME_KEYFRAME));
    }

    #[test]
    fn test_compression_stats() {
        let stats = compression_stats(1000, 300);
        assert!((stats[0] - 0.3).abs() < 0.001);
        assert!((stats[1] - 70.0).abs() < 0.1);
    }

    // ─── v0.3 Tests ───

    #[test]
    fn test_typed_frame_header() {
        let msg = build_message(FRAME_SIGNAL_SYNC, FLAG_KEYFRAME, 7, 5000, 320, 240, b"{}");
        let typed = decode_header_typed(&msg).unwrap();
        assert_eq!(typed.frame_type(), FRAME_SIGNAL_SYNC);
        assert_eq!(typed.flags(), FLAG_KEYFRAME);
        assert_eq!(typed.seq(), 7);
        assert_eq!(typed.timestamp(), 5000);
        assert_eq!(typed.width(), 320);
        assert_eq!(typed.height(), 240);
        assert_eq!(typed.payload_length(), 2);
        assert!(!typed.is_input());
        assert!(typed.is_keyframe());
        assert_eq!(typed.type_name(), "SignalSync");
    }

    #[test]
    fn test_typed_frame_header_input() {
        // Build a pointer input message using input_message
        let mut payload = Vec::new();
        payload.extend_from_slice(&50u16.to_le_bytes()); // x
        payload.extend_from_slice(&60u16.to_le_bytes()); // y
        let msg = input_message(INPUT_POINTER, 1, 100, &payload);
        let typed = decode_header_typed(&msg).unwrap();
        assert!(typed.is_input());
        assert_eq!(typed.type_name_resolved(), "Pointer");
    }

    #[test]
    fn test_decode_recording_entry() {
        let mut entry = Vec::new();
        let ts: u64 = 123456;
        entry.extend_from_slice(&ts.to_le_bytes());
        let msg = signal_diff_message(1, 100, b"hi");
        entry.extend_from_slice(&(msg.len() as u32).to_le_bytes());
        entry.extend_from_slice(&msg);

        let result = decode_recording_entry(&entry, 0);
        assert_eq!(result.len(), 4);
        assert_eq!(result[0], 0); // ts high
        assert_eq!(result[1], 123456); // ts low
        assert_eq!(result[2], 12); // msg_offset
        assert_eq!(result[3], msg.len() as u32);
    }

    #[test]
    fn test_decode_recording_entry_truncated() {
        let result = decode_recording_entry(&[0u8; 5], 0);
        assert!(result.is_empty());
    }

    #[test]
    fn test_compressed_pixel_message() {
        let fake_webp = vec![0x52, 0x49, 0x46, 0x46];
        let msg = compressed_pixel_message(1, 100, 640, 480, COMPRESSED_FMT_WEBP, &fake_webp);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_COMPRESSED as u32);
        assert_eq!(header[1], FLAG_COMPRESSED as u32);
        assert_eq!(header[4], 640);
        assert_eq!(header[5], 480);
        assert_eq!(msg[HEADER_SIZE], COMPRESSED_FMT_WEBP);
        assert_eq!(&msg[HEADER_SIZE + 1..HEADER_SIZE + 5], &fake_webp);
    }

    // ─── v0.4 Tests ───

    #[test]
    fn test_session_recorder_export() {
        let mut rec = SessionRecorder::new();
        let msg1 = signal_diff_message(1, 100, b"{}");
        let msg2 = signal_sync_message(2, 200, b"{\"a\":1}");
        rec.record(0, &msg1);
        rec.record(100, &msg2);
        assert_eq!(rec.entry_count(), 2);
        assert_eq!(rec.duration_ms(), 100);

        let exported = rec.export();
        assert_eq!(&exported[0..3], &DSREC_MAGIC);
    }

    #[test]
    fn test_session_player_load() {
        let mut rec = SessionRecorder::new();
        let msg1 = signal_diff_message(1, 100, b"hi");
        rec.record(0, &msg1);
        rec.record(500, &msg1);
        let exported = rec.export();

        let mut player = SessionPlayer::load(&exported).unwrap();
        assert_eq!(player.total_entries(), 2);

        let e1 = player.next_entry();
        assert_eq!(e1.len(), 4);
        assert_eq!(e1[3], msg1.len() as u32);

        let e2 = player.next_entry();
        assert_eq!(e2.len(), 4);

        let e3 = player.next_entry();
        assert!(e3.is_empty()); // past end
    }

    #[test]
    fn test_session_player_seek() {
        let mut rec = SessionRecorder::new();
        let msg = signal_diff_message(1, 100, b"x");
        rec.record(0, &msg);
        rec.record(100, &msg);
        rec.record(200, &msg);
        let exported = rec.export();

        let mut player = SessionPlayer::load(&exported).unwrap();
        // Seek to 100ms = 100_000 us
        player.seek_to_time(100_000);
        let e = player.next_entry();
        assert_eq!(e.len(), 4);
        // Should be the second entry (ts=100ms=100_000us)
    }

    #[test]
    fn test_adaptive_quality_tier() {
        assert_eq!(adaptive_quality_tier(10), 0);  // Full
        assert_eq!(adaptive_quality_tier(49), 0);  // Full
        assert_eq!(adaptive_quality_tier(50), 1);  // Reduced
        assert_eq!(adaptive_quality_tier(149), 1); // Reduced
        assert_eq!(adaptive_quality_tier(150), 2); // Minimal
        assert_eq!(adaptive_quality_tier(500), 2); // Minimal
    }

    #[test]
    fn test_bandwidth_kbps() {
        let kbps = bandwidth_kbps(1_000_000, 1000);
        assert!((kbps - 8000.0).abs() < 1.0, "Expected ~8000 kbps: {}", kbps);
        assert_eq!(bandwidth_kbps(0, 0), 0.0);
    }

    // ─── v0.5 Tests ───

    #[test]
    fn test_auth_challenge_message() {
        let nonce = [1u8, 2, 3, 4, 5, 6, 7, 8];
        let msg = auth_challenge_message(1, &nonce);
        let header = decode_header(&msg);
        assert_eq!(header[0], FRAME_AUTH as u32);
        assert_eq!(msg[HEADER_SIZE], 0); // phase = challenge
        assert_eq!(&msg[HEADER_SIZE + 1..HEADER_SIZE + 9], &nonce);
    }

    #[test]
    fn test_auth_response_message() {
        let nonce = [1u8; 8];
        let token = b"my-secret";
        let msg = auth_response_message(2, &nonce, token);
        assert_eq!(msg[HEADER_SIZE], 1); // phase = response
        assert_eq!(&msg[HEADER_SIZE + 9..HEADER_SIZE + 9 + token.len()], token);
    }

    #[test]
    fn test_xor_scramble_roundtrip() {
        let data = b"hello world";
        let key = b"key123";
        let scrambled = scramble(data, key);
        assert_ne!(scrambled, data.to_vec());
        let restored = descramble(&scrambled, key);
        assert_eq!(restored, data.to_vec());
    }

    #[test]
    fn test_decode_recording_metadata() {
        // Build a 24-byte metadata buffer
        let mut buf = [0u8; 24];
        buf[0..2].copy_from_slice(&1u16.to_le_bytes()); // version
        buf[2..10].copy_from_slice(&1710000000u64.to_le_bytes()); // created_at
        buf[10..14].copy_from_slice(&5000u32.to_le_bytes()); // duration_ms
        buf[14..18].copy_from_slice(&150u32.to_le_bytes()); // frame_count
        buf[18..20].copy_from_slice(&1920u16.to_le_bytes()); // width
        buf[20..22].copy_from_slice(&1080u16.to_le_bytes()); // height
        let result = decode_recording_metadata(&buf);
        assert_eq!(result.len(), 7);
        assert_eq!(result[0], 1); // version
        assert_eq!(result[3], 5000); // duration_ms
        assert_eq!(result[4], 150); // frame_count
        assert_eq!(result[5], 1920); // width
        assert_eq!(result[6], 1080); // height
    }

    // ─── v0.10 Tests ───

    #[test]
    fn test_mux_demux_message() {
        let msg = build_message(0x02, 0, 1, 0, 320, 240, &[0xBB; 5]);
        let muxed = mux_message(7, &msg);
        assert_eq!(extract_stream_id(muxed[1]), 7);
        let demuxed = demux_message(&muxed);
        assert_eq!(demuxed[0], 7); // stream_id prepended
    }

    #[test]
    fn test_frame_priority() {
        assert_eq!(frame_priority(0xF0), 0); // Keyframe = Critical
        assert_eq!(frame_priority(0x01), 1); // Pixels = High
        assert_eq!(frame_priority(0x31), 2); // SignalDiff = Normal
        assert_eq!(frame_priority(0xFE), 3); // Ping = Low
    }

    #[test]
    fn test_should_drop_frame() {
        assert!(!should_drop_frame(3, 0)); // no congestion
        assert!(should_drop_frame(3, 1));  // mild: drop low
        assert!(!should_drop_frame(0, 3)); // severe: keep critical
        assert!(should_drop_frame(1, 3));  // severe: drop high
    }

    #[test]
    fn test_stream_id_helpers() {
        let combined = embed_stream_id(0x03, 10);
        assert_eq!(extract_stream_id(combined), 10);
        assert_eq!(combined & FLAGS_MASK, 0x03);
    }

    // ─── v0.11 Tests ───

    #[test]
    fn test_frame_stats() {
        // Reset state (thread local may have state from other tests)
        record_frame_stat(1000, false);
        record_frame_stat(2000, false);
        record_frame_stat(500, true);
        let stats = get_frame_stats();
        assert!(stats[0] >= 2, "Should have at least 2 frames: {}", stats[0]);
        assert!(stats[3] >= 1, "Should have at least 1 drop: {}", stats[3]);
    }

    #[test]
    fn test_rtt_tracking() {
        record_rtt(10.0);
        record_rtt(20.0);
        record_rtt(30.0);
        let avg = get_avg_rtt();
        assert!(avg > 0.0, "Should have positive RTT: {}", avg);
        let jitter = get_jitter();
        assert!(jitter >= 0.0, "Jitter should be non-negative: {}", jitter);
    }

    #[test]
    fn test_stream_health() {
        let health = stream_health_score();
        assert!(health >= 0.0 && health <= 1.0, "Health should be 0-1: {}", health);
    }

    // ─── v0.12 Tests ───

    #[test]
    fn test_state_snapshot() {
        let state = b"test state data";
        let encoded = encode_state_snapshot(7, state);
        let decoded = decode_state_snapshot(&encoded);
        assert_eq!(decoded.len(), 4 + state.len());
        assert_eq!(u32::from_le_bytes([decoded[0], decoded[1], decoded[2], decoded[3]]), 7);
    }

    #[test]
    fn test_gap_detection() {
        let r0 = feed_seq(0);
        assert!(r0 == 0 || r0 == 1, "First feed: {}", r0); // may be Gap if state from other tests
        let r1 = feed_seq(1);
        assert!(r1 == 0 || r1 == 1, "Second: {}", r1);
    }

    #[test]
    fn test_protocol_version() {
        assert!(check_protocol_version(12));
        assert!(!check_protocol_version(11));
    }

    // ─── v0.13 Tests ───

    #[test]
    fn test_replay_buffer() {
        push_replay(&[1, 2, 3]);
        push_replay(&[4, 5, 6]);
        assert!(replay_len() >= 2, "Should have at least 2 frames");
        let range = get_replay_range(0, 1);
        assert!(range.len() > 0, "Should get data back");
    }

    #[test]
    fn test_header_serde() {
        let header = serialize_frame_header(0x02, 0x01, 42, 1000, 800, 600, 256);
        let decoded = deserialize_frame_header(&header);
        assert_eq!(decoded.len(), 7);
        assert_eq!(decoded[0], 0x02);
        assert_eq!(decoded[2], 42);
        assert_eq!(decoded[4], 800);
    }

    #[test]
    fn test_codec_registry() {
        register_codec("jpeg", 1);
        register_codec("webp", 2);
        let list = list_codecs();
        assert!(list.contains("jpeg"), "Should contain jpeg: {}", list);
    }

    // ─── v0.14 Tests ───

    #[test]
    fn test_rle_compress_decompress() {
        let data = vec![5, 5, 5, 3, 3, 1];
        let compressed = rle_compress(&data);
        let decompressed = rle_decompress(&compressed);
        assert_eq!(decompressed, data);
    }

    #[test]
    fn test_sync_drift() {
        set_client_seq(20);
        set_client_ack(15);
        assert_eq!(get_sync_drift(), 5);
    }

    #[test]
    fn test_bandwidth_limit() {
        set_bandwidth_limit(100);
        reset_bandwidth();
        assert_eq!(check_bandwidth(50), 0); // ok
        assert_eq!(check_bandwidth(60), 1); // over
        reset_bandwidth();
        assert_eq!(check_bandwidth(60), 0); // ok after reset
    }

    // ─── v0.15 Tests ───

    #[test]
    fn test_adaptive_quality() {
        // Good connection
        for _ in 0..10 { feed_quality_sample(10.0, 0.0); }
        assert_eq!(recommended_quality(), 4);
        // Bad connection
        for _ in 0..30 { feed_quality_sample(250.0, 15.0); }
        assert_eq!(recommended_quality(), 0);
    }

    #[test]
    fn test_metrics_snapshot() {
        let json = metrics_snapshot(30.0, 500000, 3, 2.5, 0.95);
        assert!(json.contains("30.0"), "Has fps: {}", json);
        assert!(json.contains("500000"), "Has bitrate: {}", json);
    }

    #[test]
    fn test_frame_transforms() {
        add_frame_transform("encode");
        add_frame_transform("compress");
        let out = apply_transforms(&[1, 2, 3]);
        assert!(out.len() > 3, "Has stage count prefix");
        let list = list_transforms();
        assert!(list.contains("encode"), "Has encode: {}", list);
    }

    // ─── v0.16 Tests ───

    #[test]
    fn test_encrypt_decrypt_frame() {
        let data = vec![1, 2, 3, 4, 5];
        let key = vec![0xAB, 0xCD];
        let enc = encrypt_frame(&data, &key);
        assert_ne!(enc, data);
        let dec = decrypt_frame(&enc, &key);
        assert_eq!(dec, data);
    }

    #[test]
    fn test_migration_handoff() {
        let state = prepare_migration(100, 3);
        let result = accept_migration(&state);
        assert_eq!(result, vec![100, 3]);
    }

    #[test]
    fn test_frame_dedup_wasm() {
        let frame = vec![99, 88, 77];
        let first = is_frame_duplicate(&frame);
        assert_eq!(first, 0);
        // Second call with same data should detect dup
        let second = is_frame_duplicate(&frame);
        assert_eq!(second, 1);
    }
}

