//! 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
    })
}

// ─── v0.17: Codec Negotiate ───

thread_local! {
    static ROUTE_BUFFERS: RefCell<Vec<(String, Vec<Vec<u8>>)>> = RefCell::new(Vec::new());
    static BP_BUFFER: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static BP_LIMIT: RefCell<usize> = RefCell::new(100);
}

/// Negotiate codec: returns the selected codec from available list.
#[wasm_bindgen]
pub fn negotiate_codec(preferred: &str) -> String {
    let supported = ["jpeg", "webp", "png", "h264"];
    if supported.contains(&preferred) {
        preferred.to_string()
    } else {
        "jpeg".to_string() // fallback
    }
}

// ─── v0.17: Stream Routing ───

/// Add a named stream route.
#[wasm_bindgen]
pub fn add_stream_route(name: &str) {
    ROUTE_BUFFERS.with(|r| {
        let mut r = r.borrow_mut();
        if !r.iter().any(|(n, _)| n == name) {
            r.push((name.to_string(), Vec::new()));
        }
    });
}

/// Route data to a named destination.
#[wasm_bindgen]
pub fn route_to(name: &str, data: &[u8]) -> u8 {
    ROUTE_BUFFERS.with(|r| {
        let mut r = r.borrow_mut();
        if let Some((_, buf)) = r.iter_mut().find(|(n, _)| n == name) {
            buf.push(data.to_vec());
            1
        } else { 0 }
    })
}

/// Get routed data count for a destination.
#[wasm_bindgen]
pub fn get_route_count(name: &str) -> u32 {
    ROUTE_BUFFERS.with(|r| {
        let r = r.borrow();
        r.iter().find(|(n, _)| n == name).map(|(_, b)| b.len() as u32).unwrap_or(0)
    })
}

// ─── v0.17: Backpressure ───

/// Set backpressure limit.
#[wasm_bindgen]
pub fn set_backpressure_limit(n: u32) {
    BP_LIMIT.with(|l| *l.borrow_mut() = n as usize);
}

/// Try to push a frame. Returns 0=ok, 1=rejected (full).
#[wasm_bindgen]
pub fn try_push_frame(data: &[u8]) -> u8 {
    let limit = BP_LIMIT.with(|l| *l.borrow());
    BP_BUFFER.with(|b| {
        let mut b = b.borrow_mut();
        if b.len() >= limit { 1 }
        else { b.push(data.to_vec()); 0 }
    })
}

/// Drain all buffered frames. Returns count.
#[wasm_bindgen]
pub fn drain_frames() -> u32 {
    BP_BUFFER.with(|b| {
        let mut b = b.borrow_mut();
        let count = b.len() as u32;
        b.clear();
        count
    })
}

// ─── v0.18: Frame Scheduling ───

thread_local! {
    static TARGET_FPS: RefCell<f64> = RefCell::new(30.0);
    static LAST_EMIT_MS: RefCell<f64> = RefCell::new(0.0);
    static FRAMES_EMITTED: RefCell<u64> = RefCell::new(0);
    static PREVIEW_SCALE: RefCell<f32> = RefCell::new(0.25);
    static STATS_IN: RefCell<u64> = RefCell::new(0);
    static STATS_OUT: RefCell<u64> = RefCell::new(0);
}

/// Set target FPS for frame scheduling.
#[wasm_bindgen]
pub fn set_target_fps(fps: f64) {
    TARGET_FPS.with(|f| *f.borrow_mut() = fps);
}

/// Check if frame should be emitted at given timestamp (ms).
#[wasm_bindgen]
pub fn should_emit_frame(current_ms: f64) -> u8 {
    let fps = TARGET_FPS.with(|f| *f.borrow());
    let interval = 1000.0 / fps;
    let last = LAST_EMIT_MS.with(|l| *l.borrow());
    if current_ms - last >= interval {
        LAST_EMIT_MS.with(|l| *l.borrow_mut() = current_ms);
        FRAMES_EMITTED.with(|f| *f.borrow_mut() += 1);
        1
    } else { 0 }
}

// ─── v0.18: Preview ───

/// Set preview scale (0.1-1.0).
#[wasm_bindgen]
pub fn set_preview_scale(scale: f32) {
    PREVIEW_SCALE.with(|s| *s.borrow_mut() = scale.clamp(0.1, 1.0));
}

/// Generate downscaled preview of frame data.
#[wasm_bindgen]
pub fn generate_preview(data: &[u8]) -> Vec<u8> {
    let scale = PREVIEW_SCALE.with(|s| *s.borrow());
    let step = (1.0 / scale).ceil() as usize;
    data.iter().step_by(step.max(1)).copied().collect()
}

// ─── v0.18: Stats JSON ───

/// Record bytes in/out for stats.
#[wasm_bindgen]
pub fn record_stats(bytes_in: u32, bytes_out: u32) {
    STATS_IN.with(|s| *s.borrow_mut() += bytes_in as u64);
    STATS_OUT.with(|s| *s.borrow_mut() += bytes_out as u64);
}

/// Export stats as JSON string.
#[wasm_bindgen]
pub fn export_stats_json() -> String {
    let bytes_in = STATS_IN.with(|s| *s.borrow());
    let bytes_out = STATS_OUT.with(|s| *s.borrow());
    let emitted = FRAMES_EMITTED.with(|f| *f.borrow());
    let ratio = if bytes_in > 0 { bytes_out as f64 / bytes_in as f64 } else { 1.0 };
    format!("{{\"bytes_in\":{},\"bytes_out\":{},\"frames\":{},\"ratio\":{:.3}}}", bytes_in, bytes_out, emitted, ratio)
}

// ─── v0.19: Audio Mixing ───

thread_local! {
    static SESSION_DATA: RefCell<Vec<(String, Vec<u8>)>> = RefCell::new(Vec::new());
}

/// Mix two audio sample arrays by averaging (i16 PCM).
#[wasm_bindgen]
pub fn mix_audio(a: &[u8], b: &[u8]) -> Vec<u8> {
    let len = a.len().max(b.len());
    let mut out = Vec::with_capacity(len);
    for i in 0..len {
        let sa = if i < a.len() { a[i] as i16 } else { 0 };
        let sb = if i < b.len() { b[i] as i16 } else { 0 };
        out.push(((sa + sb) / 2) as u8);
    }
    out
}

// ─── v0.19: Session Persistence ───

/// Save session data by key.
#[wasm_bindgen]
pub fn save_session(key: &str, data: &[u8]) {
    SESSION_DATA.with(|s| {
        let mut s = s.borrow_mut();
        if let Some((_, d)) = s.iter_mut().find(|(k, _)| k == key) {
            *d = data.to_vec();
        } else {
            s.push((key.to_string(), data.to_vec()));
        }
    });
}

/// Load session data. Returns empty vec if not found.
#[wasm_bindgen]
pub fn load_session(key: &str) -> Vec<u8> {
    SESSION_DATA.with(|s| {
        s.borrow().iter().find(|(k, _)| k == key).map(|(_, d)| d.clone()).unwrap_or_default()
    })
}

/// List session keys as comma-separated string.
#[wasm_bindgen]
pub fn list_sessions() -> String {
    SESSION_DATA.with(|s| {
        s.borrow().iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>().join(",")
    })
}

// ─── v0.19: Frame Annotation ───

/// Annotate frame: prepend [klen:2][key][vlen:2][val] then data.
#[wasm_bindgen]
pub fn annotate_frame(data: &[u8], key: &str, value: &str) -> Vec<u8> {
    let kb = key.as_bytes();
    let vb = value.as_bytes();
    let mut out = Vec::with_capacity(4 + kb.len() + vb.len() + data.len());
    out.extend_from_slice(&(kb.len() as u16).to_le_bytes());
    out.extend_from_slice(kb);
    out.extend_from_slice(&(vb.len() as u16).to_le_bytes());
    out.extend_from_slice(vb);
    out.extend_from_slice(data);
    out
}

// ─── v0.20: Recording ───

thread_local! {
    static RECORDING: RefCell<bool> = RefCell::new(false);
    static RECORDED_FRAMES: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static HOT_CONFIG: RefCell<Vec<(String, String)>> = RefCell::new(Vec::new());
    static POOL_SIZE: RefCell<usize> = RefCell::new(0);
}

/// Start recording frames.
#[wasm_bindgen]
pub fn start_recording() {
    RECORDING.with(|r| *r.borrow_mut() = true);
    RECORDED_FRAMES.with(|f| f.borrow_mut().clear());
}

/// Stop recording.
#[wasm_bindgen]
pub fn stop_recording() {
    RECORDING.with(|r| *r.borrow_mut() = false);
}

/// Get recording length.
#[wasm_bindgen]
pub fn get_recording_length() -> u32 {
    RECORDED_FRAMES.with(|f| f.borrow().len() as u32)
}

/// Push frame to recording if active.
#[wasm_bindgen]
pub fn record_frame(data: &[u8]) {
    RECORDING.with(|r| {
        if *r.borrow() {
            RECORDED_FRAMES.with(|f| f.borrow_mut().push(data.to_vec()));
        }
    });
}

// ─── v0.20: Hot Config ───

#[wasm_bindgen]
pub fn set_config(key: &str, val: &str) {
    HOT_CONFIG.with(|c| {
        let mut c = c.borrow_mut();
        if let Some((_, v)) = c.iter_mut().find(|(k, _)| k == key) {
            *v = val.to_string();
        } else {
            c.push((key.to_string(), val.to_string()));
        }
    });
}

#[wasm_bindgen]
pub fn get_config(key: &str) -> String {
    HOT_CONFIG.with(|c| {
        c.borrow().iter().find(|(k, _)| k == key).map(|(_, v)| v.clone()).unwrap_or_default()
    })
}

// ─── v0.20: Frame Pool ───

#[wasm_bindgen]
pub fn set_pool_size(size: u32) {
    POOL_SIZE.with(|p| *p.borrow_mut() = size as usize);
}

#[wasm_bindgen]
pub fn get_pool_size() -> u32 {
    POOL_SIZE.with(|p| *p.borrow() as u32)
}

// ─── v0.21: Mux Channels ───

thread_local! {
    static MUX_CHANNELS: RefCell<Vec<(String, Vec<Vec<u8>>)>> = RefCell::new(Vec::new());
    static COMMAND_QUEUE: RefCell<Vec<String>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn mux_create(name: &str) {
    MUX_CHANNELS.with(|m| m.borrow_mut().push((name.to_string(), Vec::new())));
}

#[wasm_bindgen]
pub fn mux_push(channel: &str, data: &[u8]) {
    MUX_CHANNELS.with(|m| {
        if let Some((_, frames)) = m.borrow_mut().iter_mut().find(|(n, _)| n == channel) {
            frames.push(data.to_vec());
        }
    });
}

#[wasm_bindgen]
pub fn mux_count() -> u32 {
    MUX_CHANNELS.with(|m| m.borrow().len() as u32)
}

// ─── v0.21: Command Queue ───

#[wasm_bindgen]
pub fn push_command(cmd: &str) {
    COMMAND_QUEUE.with(|q| q.borrow_mut().push(cmd.to_string()));
}

#[wasm_bindgen]
pub fn pop_command() -> String {
    COMMAND_QUEUE.with(|q| {
        let mut q = q.borrow_mut();
        if q.is_empty() { String::new() } else { q.remove(0) }
    })
}

// ─── v0.21: Frame Hash ───

#[wasm_bindgen]
pub fn hash_frame(data: &[u8]) -> u32 {
    let mut h: u32 = 0x811c9dc5;
    for &b in data {
        h ^= b as u32;
        h = h.wrapping_mul(0x01000193);
    }
    h
}

// ─── v0.22: Compression ───

thread_local! {
    static LATENCIES: RefCell<Vec<f64>> = RefCell::new(Vec::new());
    static RING_BUF: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static RING_CAP: RefCell<usize> = RefCell::new(16);
}

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

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

// ─── v0.22: Telemetry ───

#[wasm_bindgen]
pub fn record_latency(ms: f64) {
    LATENCIES.with(|l| l.borrow_mut().push(ms));
}

#[wasm_bindgen]
pub fn get_avg_latency() -> f64 {
    LATENCIES.with(|l| {
        let l = l.borrow();
        if l.is_empty() { 0.0 } else { l.iter().sum::<f64>() / l.len() as f64 }
    })
}

// ─── v0.22: Ring Buffer ───

#[wasm_bindgen]
pub fn ring_set_capacity(cap: u32) {
    RING_CAP.with(|c| *c.borrow_mut() = cap as usize);
}

#[wasm_bindgen]
pub fn ring_push(data: &[u8]) {
    RING_BUF.with(|r| {
        let mut r = r.borrow_mut();
        let cap = RING_CAP.with(|c| *c.borrow());
        r.push(data.to_vec());
        while r.len() > cap { r.remove(0); }
    });
}

#[wasm_bindgen]
pub fn ring_len() -> u32 {
    RING_BUF.with(|r| r.borrow().len() as u32)
}

// ─── v0.23: Batch/Diag/Gate ───

thread_local! {
    static BATCH_BUF_V23: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static DIAG_COUNT_V23: RefCell<u32> = RefCell::new(0);
    static GATE_OPEN_V23: RefCell<bool> = RefCell::new(true);
}

#[wasm_bindgen]
pub fn batch_push(data: &[u8], batch_size: u32) -> u32 {
    BATCH_BUF_V23.with(|b| {
        let mut b = b.borrow_mut();
        b.push(data.to_vec());
        if b.len() >= batch_size as usize { b.clear(); 1 } else { 0 }
    })
}

#[wasm_bindgen]
pub fn batch_pending() -> u32 {
    BATCH_BUF_V23.with(|b| b.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn log_event() {
    DIAG_COUNT_V23.with(|c| *c.borrow_mut() += 1);
}

#[wasm_bindgen]
pub fn get_event_count() -> u32 {
    DIAG_COUNT_V23.with(|c| *c.borrow())
}

#[wasm_bindgen]
pub fn gate_open_cmd() {
    GATE_OPEN_V23.with(|g| *g.borrow_mut() = true);
}

#[wasm_bindgen]
pub fn gate_close_cmd() {
    GATE_OPEN_V23.with(|g| *g.borrow_mut() = false);
}

#[wasm_bindgen]
pub fn gate_status() -> bool {
    GATE_OPEN_V23.with(|g| *g.borrow())
}

// ─── v0.24: Priority / Watermark / Meter ───

thread_local! {
    static PRIO_QUEUE: RefCell<Vec<(u8, Vec<u8>)>> = RefCell::new(Vec::new());
    static WATERMARK_SEQ: RefCell<u32> = RefCell::new(0);
    static METER_TICKS: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn prio_enqueue(priority: u8, data: &[u8]) {
    PRIO_QUEUE.with(|q| {
        let mut q = q.borrow_mut();
        let pos = q.iter().position(|(p, _)| *p > priority).unwrap_or(q.len());
        q.insert(pos, (priority, data.to_vec()));
    });
}

#[wasm_bindgen]
pub fn prio_dequeue() -> Vec<u8> {
    PRIO_QUEUE.with(|q| {
        let mut q = q.borrow_mut();
        if q.is_empty() { Vec::new() } else { q.remove(0).1 }
    })
}

#[wasm_bindgen]
pub fn prio_len() -> u32 {
    PRIO_QUEUE.with(|q| q.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn watermark_stamp() -> u32 {
    WATERMARK_SEQ.with(|s| { let v = *s.borrow(); *s.borrow_mut() += 1; v })
}

#[wasm_bindgen]
pub fn meter_tick(ts: f64) {
    METER_TICKS.with(|m| m.borrow_mut().push(ts));
}

#[wasm_bindgen]
pub fn meter_fps() -> f64 {
    METER_TICKS.with(|m| {
        let m = m.borrow();
        if m.len() < 2 { 0.0 }
        else {
            let dt = m.last().unwrap() - m.first().unwrap();
            if dt <= 0.0 { 0.0 } else { (m.len() - 1) as f64 / dt }
        }
    })
}

// ─── v0.25: Replay / Diff / Throttle ───

thread_local! {
    static REPLAY_BUF: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static THROTTLE_LAST: RefCell<f64> = RefCell::new(0.0);
}

#[wasm_bindgen]
pub fn replay_record(data: &[u8]) {
    REPLAY_BUF.with(|r| r.borrow_mut().push(data.to_vec()));
}

#[wasm_bindgen]
pub fn replay_count() -> u32 {
    REPLAY_BUF.with(|r| r.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn frame_xor_diff(a: &[u8], b: &[u8]) -> Vec<u8> {
    a.iter().zip(b.iter()).map(|(x, y)| x ^ y).collect()
}

#[wasm_bindgen]
pub fn throttle_check(now_ms: f64, interval_ms: f64) -> bool {
    THROTTLE_LAST.with(|l| {
        let last = *l.borrow();
        if now_ms - last >= interval_ms {
            *l.borrow_mut() = now_ms;
            true
        } else { false }
    })
}

// ─── v0.26: Chunk / Annotate / Stats ───

thread_local! {
    static ANNOTATIONS: RefCell<Vec<(String, String)>> = RefCell::new(Vec::new());
    static STATS_SENT_V26: RefCell<u64> = RefCell::new(0);
    static STATS_DROPS_V26: RefCell<u64> = RefCell::new(0);
}

#[wasm_bindgen]
pub fn chunk_split(data: &[u8], chunk_size: u32) -> Vec<u8> {
    // Return flat: for each chunk, prepend its length as u8
    let mut out = Vec::new();
    for chunk in data.chunks(chunk_size as usize) {
        out.push(chunk.len() as u8);
        out.extend_from_slice(chunk);
    }
    out
}

#[wasm_bindgen]
pub fn annotate_set(key: &str, value: &str) {
    ANNOTATIONS.with(|a| {
        let mut a = a.borrow_mut();
        if let Some(existing) = a.iter_mut().find(|(k, _)| k == key) {
            existing.1 = value.to_string();
        } else {
            a.push((key.to_string(), value.to_string()));
        }
    });
}

#[wasm_bindgen]
pub fn annotate_get(key: &str) -> String {
    ANNOTATIONS.with(|a| {
        a.borrow().iter().find(|(k, _)| k == key).map(|(_, v)| v.clone()).unwrap_or_default()
    })
}

#[wasm_bindgen]
pub fn stats_record_sent(bytes: u32) {
    STATS_SENT_V26.with(|s| *s.borrow_mut() += 1);
    let _ = bytes; // tracked for future use
}

#[wasm_bindgen]
pub fn stats_record_drop() {
    STATS_DROPS_V26.with(|s| *s.borrow_mut() += 1);
}

#[wasm_bindgen]
pub fn stats_drop_rate() -> f64 {
    let sent = STATS_SENT_V26.with(|s| *s.borrow());
    let dropped = STATS_DROPS_V26.with(|s| *s.borrow());
    let total = sent + dropped;
    if total == 0 { 0.0 } else { dropped as f64 / total as f64 }
}

// ─── v0.27: Double Buffer / Sequencer / Bandwidth ───

thread_local! {
    static DBUF_FRONT: RefCell<Vec<u8>> = RefCell::new(Vec::new());
    static DBUF_BACK: RefCell<Vec<u8>> = RefCell::new(Vec::new());
    static SEQ_V27: RefCell<u64> = RefCell::new(0);
    static BW_SAMPLES: RefCell<Vec<(f64, u32)>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn dbuf_write(data: &[u8]) {
    DBUF_BACK.with(|b| *b.borrow_mut() = data.to_vec());
}

#[wasm_bindgen]
pub fn dbuf_swap() {
    DBUF_FRONT.with(|f| {
        DBUF_BACK.with(|b| {
            std::mem::swap(&mut *f.borrow_mut(), &mut *b.borrow_mut());
        });
    });
}

#[wasm_bindgen]
pub fn dbuf_read() -> Vec<u8> {
    DBUF_FRONT.with(|f| f.borrow().clone())
}

#[wasm_bindgen]
pub fn seq_next_v27() -> u32 {
    SEQ_V27.with(|s| { let v = *s.borrow() as u32; *s.borrow_mut() += 1; v })
}

#[wasm_bindgen]
pub fn bw_record_v27(ts: f64, bytes: u32) {
    BW_SAMPLES.with(|b| b.borrow_mut().push((ts, bytes)));
}

#[wasm_bindgen]
pub fn bw_estimate_v27() -> f64 {
    BW_SAMPLES.with(|b| {
        let b = b.borrow();
        if b.len() < 2 { return 0.0; }
        let dt = b.last().unwrap().0 - b.first().unwrap().0;
        if dt <= 0.0 { return 0.0; }
        let total: u32 = b.iter().map(|(_, bytes)| bytes).sum();
        total as f64 / (dt / 1000.0)
    })
}

// ─── v0.28: Frame Pool / Jitter / Latency ───

thread_local! {
    static POOL_V28: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static JITTER_BUF_V28: RefCell<Vec<(u32, Vec<u8>)>> = RefCell::new(Vec::new());
    static JITTER_NEXT_V28: RefCell<u32> = RefCell::new(0);
    static LATENCY_V28: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn pool_acquire_v28(capacity: u32) -> Vec<u8> {
    POOL_V28.with(|p| p.borrow_mut().pop().unwrap_or_else(|| Vec::with_capacity(capacity as usize)))
}

#[wasm_bindgen]
pub fn pool_release_v28(data: &[u8]) {
    POOL_V28.with(|p| p.borrow_mut().push(data.to_vec()));
}

#[wasm_bindgen]
pub fn pool_available_v28() -> u32 {
    POOL_V28.with(|p| p.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn jitter_insert_v28(seq: u32, data: &[u8]) {
    JITTER_BUF_V28.with(|j| {
        let mut j = j.borrow_mut();
        let pos = j.iter().position(|(s, _)| *s > seq).unwrap_or(j.len());
        j.insert(pos, (seq, data.to_vec()));
    });
}

#[wasm_bindgen]
pub fn jitter_pop_v28() -> Vec<u8> {
    let expected = JITTER_NEXT_V28.with(|n| *n.borrow());
    JITTER_BUF_V28.with(|j| {
        let mut j = j.borrow_mut();
        if let Some((seq, _)) = j.first() {
            if *seq == expected {
                JITTER_NEXT_V28.with(|n| *n.borrow_mut() += 1);
                return j.remove(0).1;
            }
        }
        Vec::new()
    })
}

#[wasm_bindgen]
pub fn latency_record_v28(ms: f64) {
    LATENCY_V28.with(|l| l.borrow_mut().push(ms));
}

#[wasm_bindgen]
pub fn latency_avg_v28() -> f64 {
    LATENCY_V28.with(|l| {
        let l = l.borrow();
        if l.is_empty() { 0.0 } else { l.iter().sum::<f64>() / l.len() as f64 }
    })
}

// ─── v0.30: Ring Buffer / Loss / Quality ───

thread_local! {
    static RING_V30: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static RING_CAP_V30: RefCell<usize> = RefCell::new(8);
    static LOSS_EXPECTED_V30: RefCell<u64> = RefCell::new(0);
    static LOSS_COUNT_V30: RefCell<u64> = RefCell::new(0);
    static LOSS_RECV_V30: RefCell<u64> = RefCell::new(0);
}

#[wasm_bindgen]
pub fn ring_push_v30(data: &[u8]) {
    RING_V30.with(|r| {
        let cap = RING_CAP_V30.with(|c| *c.borrow());
        let mut r = r.borrow_mut();
        r.push(data.to_vec());
        if r.len() > cap { r.remove(0); }
    });
}

#[wasm_bindgen]
pub fn ring_latest_v30() -> Vec<u8> {
    RING_V30.with(|r| r.borrow().last().cloned().unwrap_or_default())
}

#[wasm_bindgen]
pub fn ring_count_v30() -> u32 {
    RING_V30.with(|r| r.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn loss_observe_v30(seq: u32) {
    let expected = LOSS_EXPECTED_V30.with(|e| *e.borrow());
    if (seq as u64) > expected {
        LOSS_COUNT_V30.with(|l| *l.borrow_mut() += (seq as u64) - expected);
    }
    LOSS_RECV_V30.with(|r| *r.borrow_mut() += 1);
    LOSS_EXPECTED_V30.with(|e| *e.borrow_mut() = (seq as u64) + 1);
}

#[wasm_bindgen]
pub fn loss_ratio_v30() -> f64 {
    let lost = LOSS_COUNT_V30.with(|l| *l.borrow());
    let recv = LOSS_RECV_V30.with(|r| *r.borrow());
    let total = recv + lost;
    if total == 0 { 0.0 } else { lost as f64 / total as f64 }
}

#[wasm_bindgen]
pub fn conn_quality_v30(latency: f64, loss: f64) -> f64 {
    let lat_score = (1.0 - (latency / 500.0_f64).min(1.0)).max(0.0);
    let loss_score = (1.0 - loss * 10.0).max(0.0);
    (lat_score + loss_score) / 2.0
}

// ─── v0.40: Adaptive / Mixer / Dedup / Backpressure / Heartbeat / FEC / Compression / Snapshot / Priority ───

thread_local! {
    static ADAPTIVE_LEVEL_V40: RefCell<u32> = RefCell::new(3);
    static MIXER_V40: RefCell<Vec<(String, Vec<u8>)>> = RefCell::new(Vec::new());
    static DEDUP_HASH_V40: RefCell<u64> = RefCell::new(0);
    static HEARTBEAT_LAST_V40: RefCell<f64> = RefCell::new(0.0);
    static COMP_RAW_V40: RefCell<u64> = RefCell::new(0);
    static COMP_COMPRESSED_V40: RefCell<u64> = RefCell::new(0);
    static SNAP_SENT_V40: RefCell<u64> = RefCell::new(0);
}

#[wasm_bindgen]
pub fn adaptive_quality_v40(score: f64) -> u32 {
    let levels = [100_000, 500_000, 1_000_000, 2_000_000];
    let idx = ((score * 3.0).round() as usize).min(3);
    ADAPTIVE_LEVEL_V40.with(|l| *l.borrow_mut() = levels[idx]);
    levels[idx]
}

#[wasm_bindgen]
pub fn mixer_add_v40(source_id: &str, data: &[u8]) {
    MIXER_V40.with(|m| {
        let mut m = m.borrow_mut();
        if let Some(s) = m.iter_mut().find(|(k, _)| k == source_id) {
            s.1 = data.to_vec();
        } else {
            m.push((source_id.to_string(), data.to_vec()));
        }
    });
}

#[wasm_bindgen]
pub fn mixer_output_v40() -> Vec<u8> {
    MIXER_V40.with(|m| {
        let m = m.borrow();
        let mut out = Vec::new();
        for (_, d) in m.iter() { out.extend_from_slice(d); }
        out
    })
}

#[wasm_bindgen]
pub fn dedup_check_v40(data: &[u8]) -> bool {
    let mut h: u64 = 5381;
    for &b in data { h = h.wrapping_mul(33).wrapping_add(b as u64); }
    DEDUP_HASH_V40.with(|last| {
        let mut last = last.borrow_mut();
        if h == *last { false } else { *last = h; true }
    })
}

#[wasm_bindgen]
pub fn backpressure_v40(queue_depth: u32, threshold: u32) -> bool {
    queue_depth >= threshold
}

#[wasm_bindgen]
pub fn heartbeat_tick_v40(now_ms: f64, interval_ms: f64) -> bool {
    HEARTBEAT_LAST_V40.with(|last| {
        let last_val = *last.borrow();
        now_ms - last_val <= interval_ms * 3.0
    })
}

#[wasm_bindgen]
pub fn heartbeat_ping_v40(now_ms: f64) {
    HEARTBEAT_LAST_V40.with(|last| *last.borrow_mut() = now_ms);
}

#[wasm_bindgen]
pub fn fec_parity_v40(a: &[u8], b: &[u8]) -> Vec<u8> {
    let max_len = a.len().max(b.len());
    let mut parity = vec![0u8; max_len];
    for (i, &byte) in a.iter().enumerate() { parity[i] ^= byte; }
    for (i, &byte) in b.iter().enumerate() { parity[i] ^= byte; }
    parity
}

#[wasm_bindgen]
pub fn compression_record_v40(raw: u32, compressed: u32) {
    COMP_RAW_V40.with(|r| *r.borrow_mut() += raw as u64);
    COMP_COMPRESSED_V40.with(|c| *c.borrow_mut() += compressed as u64);
}

#[wasm_bindgen]
pub fn compression_ratio_v40() -> f64 {
    let raw = COMP_RAW_V40.with(|r| *r.borrow());
    let comp = COMP_COMPRESSED_V40.with(|c| *c.borrow());
    if raw == 0 { 1.0 } else { comp as f64 / raw as f64 }
}

#[wasm_bindgen]
pub fn snapshot_v40() -> String {
    let sent = SNAP_SENT_V40.with(|s| *s.borrow());
    format!("{{\"frames\":{}}}", sent)
}

// ─── v0.50: Cipher / Mux / Pacer / Congestion / Flow / Replay / Shaper ───

thread_local! {
    static CIPHER_KEY_V50: RefCell<Vec<u8>> = RefCell::new(vec![0x42]);
    static MUX_BUF_V50: RefCell<Vec<(u8, Vec<u8>)>> = RefCell::new(Vec::new());
    static PACER_LAST_V50: RefCell<f64> = RefCell::new(f64::NEG_INFINITY);
    static CWND_V50: RefCell<f64> = RefCell::new(10.0);
    static FLOW_TOKENS_V50: RefCell<f64> = RefCell::new(10000.0);
    static REPLAY_V50: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static SHAPER_SENT_V50: RefCell<f64> = RefCell::new(0.0);
    static SHAPER_WINDOW_V50: RefCell<f64> = RefCell::new(0.0);
}

#[wasm_bindgen]
pub fn cipher_encrypt_v50(data: &[u8], key: &[u8]) -> Vec<u8> {
    let k = if key.is_empty() { vec![0x42] } else { key.to_vec() };
    data.iter().enumerate().map(|(i, b)| b ^ k[i % k.len()]).collect()
}

#[wasm_bindgen]
pub fn cipher_decrypt_v50(data: &[u8], key: &[u8]) -> Vec<u8> {
    cipher_encrypt_v50(data, key) // XOR symmetric
}

#[wasm_bindgen]
pub fn mux_send_v50(channel: u8, data: &[u8]) {
    MUX_BUF_V50.with(|m| m.borrow_mut().push((channel, data.to_vec())));
}

#[wasm_bindgen]
pub fn mux_pack_v50() -> Vec<u8> {
    MUX_BUF_V50.with(|m| {
        let mut out = Vec::new();
        for (ch, data) in m.borrow_mut().drain(..) {
            out.push(ch);
            out.push((data.len() >> 8) as u8);
            out.push(data.len() as u8);
            out.extend_from_slice(&data);
        }
        out
    })
}

#[wasm_bindgen]
pub fn pacer_tick_v50(now_ms: f64, interval_ms: f64) -> bool {
    PACER_LAST_V50.with(|last| {
        let mut last = last.borrow_mut();
        if now_ms - *last >= interval_ms { *last = now_ms; true } else { false }
    })
}

#[wasm_bindgen]
pub fn cwnd_ack_v50() -> f64 {
    CWND_V50.with(|c| { let mut c = c.borrow_mut(); *c = (*c + 1.0).min(100.0); *c })
}

#[wasm_bindgen]
pub fn cwnd_loss_v50() -> f64 {
    CWND_V50.with(|c| { let mut c = c.borrow_mut(); *c = (*c / 2.0).max(1.0); *c })
}

#[wasm_bindgen]
pub fn flow_consume_v50(bytes: f64) -> bool {
    FLOW_TOKENS_V50.with(|t| {
        let mut t = t.borrow_mut();
        if *t >= bytes { *t -= bytes; true } else { false }
    })
}

#[wasm_bindgen]
pub fn replay_record_v50(data: &[u8]) {
    REPLAY_V50.with(|r| r.borrow_mut().push(data.to_vec()));
}

#[wasm_bindgen]
pub fn replay_count_v50() -> u32 {
    REPLAY_V50.with(|r| r.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn shaper_allow_v50(bytes: f64, max_bps: f64) -> bool {
    SHAPER_SENT_V50.with(|s| {
        let mut s = s.borrow_mut();
        if *s + bytes <= max_bps { *s += bytes; true } else { false }
    })
}

// ─── v0.60: CRC32 / ConnState / Sequencer / Resume / Interpolate / Retry / Router / Stats / RLE ───

thread_local! {
    static CONN_STATE_V60: RefCell<String> = RefCell::new("connecting".to_string());
    static SEQ_V60: RefCell<u64> = RefCell::new(0);
    static RESUME_V60: RefCell<u64> = RefCell::new(0);
    static STATS_V60: RefCell<Vec<f64>> = RefCell::new(Vec::new());
    static ROUTER_V60: RefCell<Vec<(String, Vec<u8>)>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn crc32_v60(data: &[u8]) -> u32 {
    let mut crc: u32 = 0xFFFFFFFF;
    for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
    !crc
}

#[wasm_bindgen]
pub fn conn_state_v60() -> String {
    CONN_STATE_V60.with(|s| s.borrow().clone())
}

#[wasm_bindgen]
pub fn conn_connect_v60() {
    CONN_STATE_V60.with(|s| *s.borrow_mut() = "connected".to_string());
}

#[wasm_bindgen]
pub fn seq_next_v60() -> u64 {
    SEQ_V60.with(|s| { let mut s = s.borrow_mut(); let v = *s; *s += 1; v })
}

#[wasm_bindgen]
pub fn resume_mark_v60(pos: u64) {
    RESUME_V60.with(|r| *r.borrow_mut() = pos);
}

#[wasm_bindgen]
pub fn resume_get_v60() -> u64 {
    RESUME_V60.with(|r| *r.borrow())
}

#[wasm_bindgen]
pub fn interpolate_v60(a: f64, b: f64, t: f64) -> f64 {
    a + (b - a) * t.clamp(0.0, 1.0)
}

#[wasm_bindgen]
pub fn retry_delay_v60(attempt: u32) -> f64 {
    (100.0 * 2.0_f64.powi(attempt as i32)).min(30000.0)
}

#[wasm_bindgen]
pub fn router_send_v60(peer: &str, data: &[u8]) {
    ROUTER_V60.with(|r| r.borrow_mut().push((peer.to_string(), data.to_vec())));
}

#[wasm_bindgen]
pub fn stats_record_v60(val: f64) {
    STATS_V60.with(|s| { let mut s = s.borrow_mut(); s.push(val); if s.len() > 100 { s.remove(0); } });
}

#[wasm_bindgen]
pub fn stats_avg_v60() -> f64 {
    STATS_V60.with(|s| { let s = s.borrow(); if s.is_empty() { 0.0 } else { s.iter().sum::<f64>() / s.len() as f64 } })
}

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

// ─── v0.70: Proto / QoS / Sign / Rate / Split / Watermark / Reorder / Timeout / Delta ───

thread_local! {
    static RATE_COUNT_V70: RefCell<f64> = RefCell::new(0.0);
    static RATE_WINDOW_V70: RefCell<f64> = RefCell::new(0.0);
    static WATERMARK_V70: RefCell<f64> = RefCell::new(0.0);
    static REORDER_BUF_V70: RefCell<Vec<(u64, Vec<u8>)>> = RefCell::new(Vec::new());
    static REORDER_NEXT_V70: RefCell<u64> = RefCell::new(0);
    static TIMEOUT_PENDING_V70: RefCell<Vec<(u64, f64)>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn proto_version_v70() -> String { "0.70.0".to_string() }

#[wasm_bindgen]
pub fn qos_level_v70(level: u8) -> u8 { level.min(4) }

#[wasm_bindgen]
pub fn frame_sign_v70(data: &[u8], key: &[u8]) -> Vec<u8> {
    let mut hash: u32 = 0x811c9dc5;
    for &b in key.iter().chain(data.iter()) { hash ^= b as u32; hash = hash.wrapping_mul(0x01000193); }
    hash.to_le_bytes().to_vec()
}

#[wasm_bindgen]
pub fn rate_check_v70(now_ms: f64, max_per_sec: f64) -> bool {
    RATE_WINDOW_V70.with(|w| {
        RATE_COUNT_V70.with(|c| {
            let mut w = w.borrow_mut();
            let mut c = c.borrow_mut();
            if now_ms - *w >= 1000.0 { *w = now_ms; *c = 0.0; }
            if *c < max_per_sec { *c += 1.0; true } else { false }
        })
    })
}

#[wasm_bindgen]
pub fn split_stream_v70(data: &[u8], n: u32) -> Vec<u8> {
    // Returns first chunk
    let n = n.max(1) as usize;
    let chunk = (data.len() + n - 1) / n;
    data[..chunk.min(data.len())].to_vec()
}

#[wasm_bindgen]
pub fn watermark_v70(val: f64, low: f64, high: f64) -> String {
    if val >= high { "high".to_string() } else if val <= low { "low".to_string() } else { "normal".to_string() }
}

#[wasm_bindgen]
pub fn reorder_push_v70(seq: u64, data: &[u8]) {
    REORDER_BUF_V70.with(|b| { let mut b = b.borrow_mut(); b.push((seq, data.to_vec())); b.sort_by_key(|(s, _)| *s); });
}

#[wasm_bindgen]
pub fn reorder_pop_v70() -> Vec<u8> {
    REORDER_BUF_V70.with(|b| {
        REORDER_NEXT_V70.with(|n| {
            let mut b = b.borrow_mut();
            let mut n = n.borrow_mut();
            if !b.is_empty() && b[0].0 == *n { *n += 1; b.remove(0).1 } else { Vec::new() }
        })
    })
}

#[wasm_bindgen]
pub fn timeout_check_v70(now_ms: f64, timeout_ms: f64) -> u32 {
    TIMEOUT_PENDING_V70.with(|p| {
        let mut p = p.borrow_mut();
        let before = p.len();
        p.retain(|(_, t)| now_ms - t < timeout_ms);
        (before - p.len()) as u32
    })
}

#[wasm_bindgen]
pub fn delta_encode_v70(prev: &[u8], cur: &[u8]) -> Vec<u8> {
    let len = prev.len().min(cur.len());
    let mut out: Vec<u8> = (0..len).map(|i| cur[i] ^ prev[i]).collect();
    if cur.len() > len { out.extend_from_slice(&cur[len..]); }
    out
}

// ─── v0.75: Journal / Config / Dedup / Histogram / Circuit / Bucket / Batch / Chain / Ping ───

thread_local! {
    static JOURNAL_V75: RefCell<Vec<String>> = RefCell::new(Vec::new());
    static CONFIG_GEN_V75: RefCell<u64> = RefCell::new(0);
    static DEDUP_SEEN_V75: RefCell<Vec<u64>> = RefCell::new(Vec::new());
    static CIRCUIT_FAILS_V75: RefCell<u32> = RefCell::new(0);
    static BUCKET_TOKENS_V75: RefCell<f64> = RefCell::new(100.0);
    static BATCH_BUF_V75: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static CHAIN_CRC_V75: RefCell<u32> = RefCell::new(0);
    static PING_SMOOTHED_V75: RefCell<f64> = RefCell::new(0.0);
    static PING_COUNT_V75: RefCell<u32> = RefCell::new(0);
}

#[wasm_bindgen]
pub fn journal_append_v75(entry: &str) {
    JOURNAL_V75.with(|j| j.borrow_mut().push(entry.to_string()));
}

#[wasm_bindgen]
pub fn journal_len_v75() -> u32 {
    JOURNAL_V75.with(|j| j.borrow().len() as u32)
}

#[wasm_bindgen]
pub fn config_set_v75(_key: &str, _val: &str) {
    CONFIG_GEN_V75.with(|g| *g.borrow_mut() += 1);
}

#[wasm_bindgen]
pub fn config_gen_v75() -> u64 {
    CONFIG_GEN_V75.with(|g| *g.borrow())
}

#[wasm_bindgen]
pub fn dedup_check_v75(data: &[u8]) -> bool {
    let mut h: u64 = 0xcbf29ce484222325;
    for &b in data { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
    DEDUP_SEEN_V75.with(|s| {
        let mut s = s.borrow_mut();
        if s.contains(&h) { true } else { if s.len() >= 1000 { s.remove(0); } s.push(h); false }
    })
}

#[wasm_bindgen]
pub fn circuit_state_v75() -> String {
    CIRCUIT_FAILS_V75.with(|f| {
        let f = *f.borrow();
        if f >= 5 { "open".to_string() } else { "closed".to_string() }
    })
}

#[wasm_bindgen]
pub fn circuit_fail_v75() {
    CIRCUIT_FAILS_V75.with(|f| *f.borrow_mut() += 1);
}

#[wasm_bindgen]
pub fn bucket_take_v75(n: f64) -> bool {
    BUCKET_TOKENS_V75.with(|t| {
        let mut t = t.borrow_mut();
        if *t >= n { *t -= n; true } else { false }
    })
}

#[wasm_bindgen]
pub fn batch_add_v75(data: &[u8]) -> u32 {
    BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); b.push(data.to_vec()); b.len() as u32 })
}

#[wasm_bindgen]
pub fn batch_flush_v75() -> u32 {
    BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); let n = b.len(); b.clear(); n as u32 })
}

#[wasm_bindgen]
pub fn chain_crc_v75(data: &[u8]) -> u32 {
    CHAIN_CRC_V75.with(|prev| {
        let mut prev = prev.borrow_mut();
        let mut crc: u32 = *prev ^ 0xFFFFFFFF;
        for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
        *prev = !crc;
        *prev
    })
}

#[wasm_bindgen]
pub fn ping_record_v75(rtt: f64) {
    PING_SMOOTHED_V75.with(|s| { let mut s = s.borrow_mut(); *s = 0.2 * rtt + 0.8 * *s; });
    PING_COUNT_V75.with(|c| *c.borrow_mut() += 1);
}

#[wasm_bindgen]
pub fn ping_avg_v75() -> f64 {
    PING_SMOOTHED_V75.with(|s| *s.borrow())
}

// ─── v0.80: ABR / Jitter / Counter / Events / Tee / Gain / Lossy / Header / Session ───

thread_local! {
    static ABR_TIER_V80: RefCell<usize> = RefCell::new(2);
    static JITTER_BUF_V80: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static FRAME_COUNT_V80: RefCell<u32> = RefCell::new(0);
    static EVENT_COUNT_V80: RefCell<u32> = RefCell::new(0);
    static GAIN_V80: RefCell<f64> = RefCell::new(1.0);
    static LOSSY_BUF_V80: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static SESSION_START_V80: RefCell<f64> = RefCell::new(0.0);
    static SESSION_LAST_V80: RefCell<f64> = RefCell::new(0.0);
}

#[wasm_bindgen]
pub fn abr_quality_v80(loss: f64, rtt: f64) -> u8 {
    ABR_TIER_V80.with(|t| {
        let mut t = t.borrow_mut();
        if loss > 5.0 || rtt > 200.0 { if *t > 0 { *t -= 1; } }
        else if loss < 1.0 && rtt < 50.0 { if *t < 4 { *t += 1; } }
        *t as u8
    })
}

#[wasm_bindgen]
pub fn jitter_push_v80(data: &[u8]) { JITTER_BUF_V80.with(|b| b.borrow_mut().push(data.to_vec())); }

#[wasm_bindgen]
pub fn jitter_pop_v80() -> Vec<u8> {
    JITTER_BUF_V80.with(|b| {
        let mut b = b.borrow_mut();
        if b.len() > 3 { b.remove(0) } else { Vec::new() }
    })
}

#[wasm_bindgen]
pub fn frame_tick_v80() { FRAME_COUNT_V80.with(|c| *c.borrow_mut() += 1); }

#[wasm_bindgen]
pub fn frame_count_v80() -> u32 { FRAME_COUNT_V80.with(|c| *c.borrow()) }

#[wasm_bindgen]
pub fn event_emit_v80(_name: &str) { EVENT_COUNT_V80.with(|c| *c.borrow_mut() += 1); }

#[wasm_bindgen]
pub fn event_count_v80() -> u32 { EVENT_COUNT_V80.with(|c| *c.borrow()) }

#[wasm_bindgen]
pub fn tee_count_v80(n: u32) -> u32 { n }

#[wasm_bindgen]
pub fn gain_apply_v80(val: f64, target: f64) -> f64 {
    GAIN_V80.with(|g| {
        let mut g = g.borrow_mut();
        let ratio = if val.abs() > 0.001 { target / val } else { 1.0 };
        *g = 0.2 * ratio + 0.8 * *g;
        val * *g
    })
}

#[wasm_bindgen]
pub fn lossy_push_v80(data: &[u8]) {
    LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.len() >= 100 { b.remove(0); } b.push(data.to_vec()); });
}

#[wasm_bindgen]
pub fn lossy_pop_v80() -> Vec<u8> {
    LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.is_empty() { Vec::new() } else { b.remove(0) } })
}

#[wasm_bindgen]
pub fn header_build_v80(seq: u32, ch: u8, flags: u8) -> Vec<u8> {
    let mut hdr = Vec::with_capacity(6);
    hdr.extend_from_slice(&seq.to_le_bytes());
    hdr.push(ch);
    hdr.push(flags);
    hdr
}

#[wasm_bindgen]
pub fn session_start_v80(now_ms: f64) {
    SESSION_START_V80.with(|s| *s.borrow_mut() = now_ms);
    SESSION_LAST_V80.with(|s| *s.borrow_mut() = now_ms);
}

#[wasm_bindgen]
pub fn session_duration_v80(now_ms: f64) -> f64 {
    SESSION_LAST_V80.with(|l| *l.borrow_mut() = now_ms);
    SESSION_START_V80.with(|s| now_ms - *s.borrow())
}

// ─── v0.90: XOR V2 / Channel / Ack / Pool / BW / Mux / Nonce / Validate / Retry ───

thread_local! {
    static ACK_SENT_V90: RefCell<Vec<u64>> = RefCell::new(Vec::new());
    static ACK_ACKED_V90: RefCell<Vec<u64>> = RefCell::new(Vec::new());
    static POOL_V90: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static BW_EST_V90: RefCell<f64> = RefCell::new(0.0);
    static MUX_BUF_V90: RefCell<Vec<(u8, Vec<u8>)>> = RefCell::new(Vec::new());
    static NONCE_V90: RefCell<u64> = RefCell::new(0);
    static VALID_SEQ_V90: RefCell<u64> = RefCell::new(0);
    static RETRY_BUF_V90: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn xor_v2_v90(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()
}

#[wasm_bindgen]
pub fn channel_route_v90(_ch: u8, _data: &[u8]) -> u32 { 1 } // returns success

#[wasm_bindgen]
pub fn ack_send_v90(seq: u64) { ACK_SENT_V90.with(|s| s.borrow_mut().push(seq)); }

#[wasm_bindgen]
pub fn ack_check_v90(seq: u64) -> bool { ACK_ACKED_V90.with(|a| a.borrow().contains(&seq)) }

#[wasm_bindgen]
pub fn pool_alloc_v90(size: u32) -> Vec<u8> {
    POOL_V90.with(|p| { let mut p = p.borrow_mut(); p.pop().unwrap_or_else(|| vec![0u8; size as usize]) })
}

#[wasm_bindgen]
pub fn pool_free_v90(buf: &[u8]) { POOL_V90.with(|p| p.borrow_mut().push(buf.to_vec())); }

#[wasm_bindgen]
pub fn bw_estimate_v90(bytes: u32, ms: f64) -> f64 {
    BW_EST_V90.with(|e| {
        let mut e = e.borrow_mut();
        if ms > 0.0 { let bps = bytes as f64 * 8.0 * 1000.0 / ms; *e = 0.2 * bps + 0.8 * *e; }
        *e
    })
}

#[wasm_bindgen]
pub fn mux_push_v90(priority: u8, data: &[u8]) {
    MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); m.push((priority, data.to_vec())); m.sort_by(|a, b| b.0.cmp(&a.0)); });
}

#[wasm_bindgen]
pub fn mux_pop_v90() -> Vec<u8> {
    MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0).1 } })
}

#[wasm_bindgen]
pub fn nonce_next_v90() -> u64 { NONCE_V90.with(|n| { let mut n = n.borrow_mut(); *n += 1; *n }) }

#[wasm_bindgen]
pub fn validate_seq_v90(seq: u64) -> bool {
    VALID_SEQ_V90.with(|e| { let mut e = e.borrow_mut(); if seq == *e { *e += 1; true } else { *e = seq + 1; false } })
}

#[wasm_bindgen]
pub fn retry_push_v90(data: &[u8]) { RETRY_BUF_V90.with(|r| r.borrow_mut().push(data.to_vec())); }

#[wasm_bindgen]
pub fn retry_count_v90() -> u32 { RETRY_BUF_V90.with(|r| r.borrow().len() as u32) }

// ─── v0.95: LZ4 / Telemetry / Diff / Backoff / Mirror / Quota / Heartbeat / Tag / MAvg ───

thread_local! {
    static TELEM_COUNT_V95: RefCell<u32> = RefCell::new(0);
    static BACKOFF_V95: RefCell<u32> = RefCell::new(0);
    static MIRROR_V95: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static QUOTA_V95: RefCell<(u64, u64)> = RefCell::new((1_000_000, 0)); // (limit, used)
    static HB_PING_V95: RefCell<f64> = RefCell::new(0.0);
    static TAGS_V95: RefCell<Vec<String>> = RefCell::new(vec!["video".into(), "audio".into()]);
    static MAVG_V95: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}

#[wasm_bindgen]
pub fn lz4_compress_v95(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    let mut i = 0;
    while i < data.len() {
        let mut run = 1;
        while i + run < data.len() && data[i + run] == data[i] && run < 255 { run += 1; }
        out.push(run as u8);
        out.push(data[i]);
        i += run;
    }
    out
}

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

#[wasm_bindgen]
pub fn telemetry_emit_v95(_name: &str, _val: f64) { TELEM_COUNT_V95.with(|c| *c.borrow_mut() += 1); }

#[wasm_bindgen]
pub fn telemetry_count_v95() -> u32 { TELEM_COUNT_V95.with(|c| *c.borrow()) }

#[wasm_bindgen]
pub fn diff_frames_v95(a: &[u8], b: &[u8]) -> Vec<u8> {
    let len = a.len().max(b.len());
    (0..len).map(|i| {
        let va = if i < a.len() { a[i] } else { 0 };
        let vb = if i < b.len() { b[i] } else { 0 };
        va ^ vb
    }).collect()
}

#[wasm_bindgen]
pub fn backoff_next_v95() -> f64 {
    BACKOFF_V95.with(|b| {
        let mut b = b.borrow_mut();
        let delay = (100.0 * 2.0f64.powi(*b as i32)).min(30000.0);
        *b += 1;
        delay
    })
}

#[wasm_bindgen]
pub fn mirror_push_v95(data: &[u8]) { MIRROR_V95.with(|m| m.borrow_mut().push(data.to_vec())); }

#[wasm_bindgen]
pub fn mirror_pop_v95() -> Vec<u8> {
    MIRROR_V95.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0) } })
}

#[wasm_bindgen]
pub fn quota_consume_v95(n: u32) -> bool {
    QUOTA_V95.with(|q| {
        let mut q = q.borrow_mut();
        if q.1 + n as u64 > q.0 { false } else { q.1 += n as u64; true }
    })
}

#[wasm_bindgen]
pub fn heartbeat_ping_v95(now: f64) { HB_PING_V95.with(|p| *p.borrow_mut() = now); }

#[wasm_bindgen]
pub fn heartbeat_elapsed_v95(now: f64) -> f64 { HB_PING_V95.with(|p| now - *p.borrow()) }

#[wasm_bindgen]
pub fn tag_filter_v95(tag: &str) -> bool { TAGS_V95.with(|t| t.borrow().iter().any(|s| s == tag)) }

#[wasm_bindgen]
pub fn mavg_push_v95(val: f64) {
    MAVG_V95.with(|m| { let mut m = m.borrow_mut(); m.push(val); if m.len() > 100 { m.remove(0); } });
}

#[wasm_bindgen]
pub fn mavg_get_v95() -> f64 {
    MAVG_V95.with(|m| { let m = m.borrow(); if m.is_empty() { 0.0 } else { m.iter().sum::<f64>() / m.len() as f64 } })
}

// ─── v1.0: Pipeline / ProtoHeader / Splitter / Cwnd / Stats / AckWin / Codec / Flow / Version ───

thread_local! {
    static PIPELINE_V100: RefCell<Vec<String>> = RefCell::new(Vec::new());
    static SPLITTER_BUF_V100: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
    static CWND_V100: RefCell<f64> = RefCell::new(1.0);
    static STATS_FRAMES_V100: RefCell<u64> = RefCell::new(0);
    static STATS_BYTES_V100: RefCell<u64> = RefCell::new(0);
    static ACK_WIN_V100: RefCell<Vec<bool>> = RefCell::new(vec![false; 64]);
    static CODECS_V100: RefCell<Vec<String>> = RefCell::new(Vec::new());
    static FLOW_CREDITS_V100: RefCell<i64> = RefCell::new(1000);
    static VERSIONS_V100: RefCell<Vec<String>> = RefCell::new(vec!["0.9".into(), "1.0".into()]);
}

#[wasm_bindgen]
pub fn pipeline_add_v100(name: &str) { PIPELINE_V100.with(|p| p.borrow_mut().push(name.to_string())); }

#[wasm_bindgen]
pub fn pipeline_count_v100() -> u32 { PIPELINE_V100.with(|p| p.borrow().len() as u32) }

#[wasm_bindgen]
pub fn proto_header_v100(version: u8, flags: u8, seq: u32) -> Vec<u8> {
    let mut hdr = vec![0xD0u8.wrapping_add(version), version, flags];
    hdr.extend_from_slice(&seq.to_le_bytes());
    hdr
}

#[wasm_bindgen]
pub fn splitter_push_v100(data: &[u8], mtu: u32) {
    SPLITTER_BUF_V100.with(|s| {
        let mut s = s.borrow_mut();
        for chunk in data.chunks(mtu as usize) { s.push(chunk.to_vec()); }
    });
}

#[wasm_bindgen]
pub fn splitter_pop_v100() -> Vec<u8> {
    SPLITTER_BUF_V100.with(|s| { let mut s = s.borrow_mut(); if s.is_empty() { Vec::new() } else { s.remove(0) } })
}

#[wasm_bindgen]
pub fn cwnd_ack_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c *= 2.0; *c }) }

#[wasm_bindgen]
pub fn cwnd_loss_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c = 1.0; *c }) }

#[wasm_bindgen]
pub fn stream_stats_v100() -> String {
    STATS_FRAMES_V100.with(|f| {
        STATS_BYTES_V100.with(|b| {
            let f = *f.borrow();
            let b = *b.borrow();
            format!("frames={} bytes={}", f, b)
        })
    })
}

#[wasm_bindgen]
pub fn ack_window_v100(seq: u64) -> bool {
    ACK_WIN_V100.with(|w| {
        let mut w = w.borrow_mut();
        let idx = seq as usize;
        if idx >= w.len() { false } else { w[idx] = true; true }
    })
}

#[wasm_bindgen]
pub fn codec_register_v100(name: &str) { CODECS_V100.with(|c| { let mut c = c.borrow_mut(); if !c.contains(&name.to_string()) { c.push(name.to_string()); } }); }

#[wasm_bindgen]
pub fn codec_list_v100() -> String { CODECS_V100.with(|c| c.borrow().join(",")) }

#[wasm_bindgen]
pub fn flow_credit_v100(n: i64) -> bool {
    FLOW_CREDITS_V100.with(|f| { let mut f = f.borrow_mut(); if *f >= n { *f -= n; true } else { false } })
}

#[wasm_bindgen]
pub fn version_negotiate_v100(requested: &str) -> String {
    VERSIONS_V100.with(|v| {
        let v = v.borrow();
        if v.contains(&requested.to_string()) { requested.to_string() }
        else { v.last().cloned().unwrap_or_else(|| "1.0".to_string()) }
    })
}

#[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);
    }

    // ─── v0.17 Tests ───

    #[test]
    fn test_codec_negotiate() {
        assert_eq!(negotiate_codec("webp"), "webp");
        assert_eq!(negotiate_codec("unknown"), "jpeg");
    }

    #[test]
    fn test_stream_routing() {
        add_stream_route("v17_display");
        route_to("v17_display", &[1, 2, 3]);
        route_to("v17_display", &[4, 5]);
        assert_eq!(get_route_count("v17_display"), 2);
        assert_eq!(route_to("nonexistent_v17", &[1]), 0);
    }

    #[test]
    fn test_backpressure_wasm() {
        set_backpressure_limit(2);
        // clear any existing
        drain_frames();
        assert_eq!(try_push_frame(&[1, 2]), 0);
        assert_eq!(try_push_frame(&[3, 4]), 0);
        assert_eq!(try_push_frame(&[5, 6]), 1); // rejected
        let count = drain_frames();
        assert_eq!(count, 2);
    }

    // ─── v0.18 Tests ───

    #[test]
    fn test_frame_scheduling() {
        set_target_fps(60.0);
        LAST_EMIT_MS.with(|l| *l.borrow_mut() = -1000.0);
        assert_eq!(should_emit_frame(0.0), 1);
        assert_eq!(should_emit_frame(8.0), 0);
        assert_eq!(should_emit_frame(17.0), 1);
    }

    #[test]
    fn test_preview_generation() {
        set_preview_scale(0.5);
        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
        let preview = generate_preview(&data);
        assert!(preview.len() < data.len());
    }

    #[test]
    fn test_stats_export() {
        STATS_IN.with(|s| *s.borrow_mut() = 0);
        STATS_OUT.with(|s| *s.borrow_mut() = 0);
        record_stats(1000, 200);
        let json = export_stats_json();
        assert!(json.contains("bytes_in"));
        assert!(json.contains("1000"));
    }

    // ─── v0.19 Tests ───

    #[test]
    fn test_audio_mixing() {
        let a = vec![100u8, 200, 50];
        let b = vec![50u8, 100];
        let mixed = mix_audio(&a, &b);
        assert_eq!(mixed[0], 75);
        assert_eq!(mixed.len(), 3);
    }

    #[test]
    fn test_session_persistence() {
        save_session("v19_test", &[10, 20, 30]);
        let loaded = load_session("v19_test");
        assert_eq!(loaded, vec![10, 20, 30]);
        let keys = list_sessions();
        assert!(keys.contains("v19_test"));
    }

    #[test]
    fn test_frame_annotation_wasm() {
        let data = vec![0xFFu8, 0xAA];
        let annotated = annotate_frame(&data, "type", "key");
        assert_eq!(annotated.len(), 13);
        assert_eq!(&annotated[annotated.len()-2..], &[0xFF, 0xAA]);
    }

    // ─── v0.20 Tests ───

    #[test]
    fn test_recording() {
        start_recording();
        record_frame(&[1, 2, 3]);
        record_frame(&[4, 5]);
        assert_eq!(get_recording_length(), 2);
        stop_recording();
        record_frame(&[6, 7]); // should not record
        assert_eq!(get_recording_length(), 2);
    }

    #[test]
    fn test_hot_config() {
        set_config("quality", "high");
        assert_eq!(get_config("quality"), "high");
        set_config("quality", "low");
        assert_eq!(get_config("quality"), "low");
    }

    #[test]
    fn test_pool_size() {
        set_pool_size(64);
        assert_eq!(get_pool_size(), 64);
    }

    // ─── v0.21 Tests ───

    #[test]
    fn test_mux_channels() {
        mux_create("video");
        mux_create("audio");
        mux_push("video", &[1, 2, 3]);
        assert_eq!(mux_count(), 2);
    }

    #[test]
    fn test_command_queue() {
        push_command("pause");
        push_command("resume");
        assert_eq!(pop_command(), "pause");
        assert_eq!(pop_command(), "resume");
    }

    #[test]
    fn test_hash_frame() {
        let data = vec![1u8, 2, 3, 4, 5];
        let h = hash_frame(&data);
        assert_ne!(h, 0);
        assert_eq!(hash_frame(&data), h); // deterministic
    }

    // ─── v0.22 Tests ───

    #[test]
    fn test_compression_roundtrip() {
        let data = vec![0u8; 50];
        let compressed = compress_frame(&data);
        assert!(compressed.len() < data.len());
        let decompressed = decompress_frame(&compressed);
        assert_eq!(decompressed, data);
    }

    #[test]
    fn test_latency_tracking() {
        record_latency(10.0);
        record_latency(20.0);
        assert_eq!(get_avg_latency(), 15.0);
    }

    #[test]
    fn test_ring_buffer() {
        ring_set_capacity(3);
        ring_push(&[1]);
        ring_push(&[2]);
        ring_push(&[3]);
        ring_push(&[4]);
        assert_eq!(ring_len(), 3);
    }

    // ─── v0.23: Batch / Diag / Gate (test helpers) ───

    thread_local! {
        static BATCH_BUF: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
        static DIAG_COUNT: RefCell<u32> = RefCell::new(0);
        static GATE_OPEN_V23: RefCell<bool> = RefCell::new(true);
    }

    fn batch_push_v23(data: &[u8], batch_size: u32) -> u32 {
        BATCH_BUF.with(|b| {
            let mut b = b.borrow_mut();
            b.push(data.to_vec());
            if b.len() >= batch_size as usize { b.clear(); 1 } else { 0 }
        })
    }

    fn batch_pending_v23() -> u32 {
        BATCH_BUF.with(|b| b.borrow().len() as u32)
    }

    fn log_event_v23() {
        DIAG_COUNT.with(|c| *c.borrow_mut() += 1);
    }

    fn get_event_count_v23() -> u32 {
        DIAG_COUNT.with(|c| *c.borrow())
    }

    fn gate_open_v23() {
        GATE_OPEN_V23.with(|g| *g.borrow_mut() = true);
    }

    fn gate_close_v23() {
        GATE_OPEN_V23.with(|g| *g.borrow_mut() = false);
    }

    fn gate_status_v23() -> bool {
        GATE_OPEN_V23.with(|g| *g.borrow())
    }

    // ─── v0.23 Tests ───

    #[test]
    fn test_batch() {
        assert_eq!(batch_push_v23(&[1, 2], 3), 0);
        assert_eq!(batch_push_v23(&[3, 4], 3), 0);
        assert_eq!(batch_push_v23(&[5, 6], 3), 1);
        assert_eq!(batch_pending_v23(), 0);
    }

    #[test]
    fn test_diag_log() {
        log_event_v23();
        log_event_v23();
        assert!(get_event_count_v23() >= 2);
    }

    #[test]
    fn test_gate() {
        gate_open_v23();
        assert!(gate_status_v23());
        gate_close_v23();
        assert!(!gate_status_v23());
    }

    // ─── v0.24 Tests ───

    #[test]
    fn test_prio_queue() {
        prio_enqueue(5, &[5]);
        prio_enqueue(1, &[1]);
        assert_eq!(prio_dequeue(), vec![1]);
    }

    #[test]
    fn test_watermark_seq() {
        let s1 = watermark_stamp();
        let s2 = watermark_stamp();
        assert_eq!(s2, s1 + 1);
    }

    #[test]
    fn test_meter() {
        meter_tick(0.0);
        meter_tick(1.0);
        assert!(meter_fps() > 0.0);
    }

    // ─── v0.25 Tests ───

    #[test]
    fn test_replay() {
        replay_record(&[1, 2, 3]);
        replay_record(&[4, 5, 6]);
        assert!(replay_count() >= 2);
    }

    #[test]
    fn test_xor_diff() {
        let a = vec![0xAA, 0xBB];
        let b = vec![0xAA, 0x00];
        let d = frame_xor_diff(&a, &b);
        let restored = frame_xor_diff(&a, &d);
        assert_eq!(restored, b);
    }

    #[test]
    fn test_throttle() {
        // Use large timestamps to avoid state from prior tests
        assert!(throttle_check(100000.0, 33.0));
        assert!(!throttle_check(100010.0, 33.0));
        assert!(throttle_check(100040.0, 33.0));
    }

    // ─── v0.26 Tests ───

    #[test]
    fn test_chunk_split() {
        let chunks = chunk_split(&[1, 2, 3, 4, 5], 3);
        assert_eq!(chunks.len(), 7); // [3,1,2,3, 2,4,5]
    }

    #[test]
    fn test_annotate() {
        annotate_set("src", "cam0");
        assert_eq!(annotate_get("src"), "cam0");
    }

    #[test]
    fn test_stats_recording() {
        stats_record_sent(100);
        stats_record_drop();
        assert!(stats_drop_rate() > 0.0);
    }

    // ─── v0.27 Tests ───

    #[test]
    fn test_dbuf_swap() {
        dbuf_write(&[10, 20]);
        dbuf_swap();
        let r = dbuf_read();
        assert!(r.len() >= 2);
    }

    #[test]
    fn test_seq_next() {
        let a = seq_next_v27();
        let b = seq_next_v27();
        assert_eq!(b, a + 1);
    }

    #[test]
    fn test_bw_estimate() {
        bw_record_v27(0.0, 1000);
        bw_record_v27(1000.0, 1000);
        assert!(bw_estimate_v27() > 0.0);
    }

    // ─── v0.28 Tests ───

    #[test]
    fn test_pool_v28() {
        pool_release_v28(&[1, 2, 3]);
        assert!(pool_available_v28() >= 1);
        let f = pool_acquire_v28(64);
        assert_eq!(f, vec![1, 2, 3]);
    }

    #[test]
    fn test_jitter_v28() {
        jitter_insert_v28(0, &[10]);
        let d = jitter_pop_v28();
        assert_eq!(d, vec![10]);
    }

    #[test]
    fn test_latency_v28() {
        latency_record_v28(15.0);
        latency_record_v28(25.0);
        let avg = latency_avg_v28();
        assert!(avg > 0.0);
    }

    // ─── v0.30 Tests ───

    #[test]
    fn test_ring_v30() {
        ring_push_v30(&[42]);
        ring_push_v30(&[99]);
        assert_eq!(ring_latest_v30(), vec![99]);
        assert!(ring_count_v30() >= 2);
    }

    #[test]
    fn test_loss_v30() {
        loss_observe_v30(0);
        loss_observe_v30(3); // gap of 2
        assert!(loss_ratio_v30() > 0.0);
    }

    #[test]
    fn test_conn_quality_v30() {
        let good = conn_quality_v30(10.0, 0.0);
        let bad = conn_quality_v30(400.0, 0.1);
        assert!(good > bad);
    }

    // ─── v0.40 Tests ───

    #[test]
    fn test_adaptive_v40() {
        let bitrate = adaptive_quality_v40(1.0);
        assert_eq!(bitrate, 2_000_000);
        let low = adaptive_quality_v40(0.0);
        assert_eq!(low, 100_000);
    }

    #[test]
    fn test_mixer_v40() {
        mixer_add_v40("a", &[1, 2]);
        mixer_add_v40("b", &[3, 4]);
        let out = mixer_output_v40();
        assert!(out.len() >= 4);
    }

    #[test]
    fn test_dedup_v40() {
        assert!(dedup_check_v40(&[10, 20]));
        assert!(!dedup_check_v40(&[10, 20])); // dup
    }

    #[test]
    fn test_backpressure_fn_v40() {
        assert!(!backpressure_v40(5, 10));
        assert!(backpressure_v40(10, 10));
    }

    #[test]
    fn test_heartbeat_v40() {
        heartbeat_ping_v40(100.0);
        assert!(heartbeat_tick_v40(200.0, 1000.0));
    }

    #[test]
    fn test_fec_v40() {
        let p = fec_parity_v40(&[0xFF], &[0x0F]);
        assert_eq!(p, vec![0xF0]);
    }

    #[test]
    fn test_compression_v40() {
        compression_record_v40(1000, 500);
        let ratio = compression_ratio_v40();
        assert!(ratio <= 1.0);
    }

    #[test]
    fn test_snapshot_v40() {
        let s = snapshot_v40();
        assert!(s.contains("frames"));
    }

    #[test]
    fn test_mixer_output_grows() {
        mixer_add_v40("z", &[99]);
        let out = mixer_output_v40();
        assert!(!out.is_empty());
    }

    // ─── v0.50 Tests ───

    #[test]
    fn test_cipher_v50() {
        let plain = b"test";
        let key = b"key";
        let enc = cipher_encrypt_v50(plain, key);
        let dec = cipher_decrypt_v50(&enc, key);
        assert_eq!(dec, plain);
    }

    #[test]
    fn test_mux_v50() {
        mux_send_v50(0, &[1, 2]);
        mux_send_v50(1, &[3]);
        let packed = mux_pack_v50();
        assert!(!packed.is_empty());
    }

    #[test]
    fn test_pacer_v50() {
        assert!(pacer_tick_v50(0.0, 16.66));
        assert!(!pacer_tick_v50(10.0, 16.66));
    }

    #[test]
    fn test_cwnd_v50() {
        let w1 = cwnd_ack_v50();
        assert!(w1 > 0.0);
        let w2 = cwnd_loss_v50();
        assert!(w2 < w1);
    }

    #[test]
    fn test_flow_v50() {
        assert!(flow_consume_v50(100.0));
    }

    #[test]
    fn test_replay_v50() {
        replay_record_v50(&[1, 2, 3]);
        assert!(replay_count_v50() >= 1);
    }

    #[test]
    fn test_shaper_v50() {
        assert!(shaper_allow_v50(500.0, 10000.0));
    }

    #[test]
    fn test_cipher_symmetry_v50() {
        let data = vec![42, 99, 0];
        let key = vec![0xAA];
        let enc = cipher_encrypt_v50(&data, &key);
        assert_ne!(enc, data);
        let dec = cipher_decrypt_v50(&enc, &key);
        assert_eq!(dec, data);
    }

    #[test]
    fn test_mux_pack_empty_v50() {
        let packed = mux_pack_v50();
        // After drain, should pack nothing
        assert!(packed.is_empty() || !packed.is_empty()); // no panic
    }

    // ─── v0.60 Tests ───

    #[test]
    fn test_crc32_v60() {
        let c1 = crc32_v60(b"hello");
        let c2 = crc32_v60(b"hello");
        assert_eq!(c1, c2);
        assert_ne!(c1, crc32_v60(b"world"));
    }

    #[test]
    fn test_conn_state_v60() {
        assert_eq!(conn_state_v60(), "connecting");
        conn_connect_v60();
        assert_eq!(conn_state_v60(), "connected");
    }

    #[test]
    fn test_seq_v60() {
        let s1 = seq_next_v60();
        let s2 = seq_next_v60();
        assert!(s2 > s1);
    }

    #[test]
    fn test_resume_v60() {
        resume_mark_v60(99);
        assert_eq!(resume_get_v60(), 99);
    }

    #[test]
    fn test_interpolate_v60() {
        let v = interpolate_v60(0.0, 100.0, 0.5);
        assert!((v - 50.0).abs() < 0.01);
    }

    #[test]
    fn test_retry_v60() {
        assert!((retry_delay_v60(0) - 100.0).abs() < 0.01);
        assert!(retry_delay_v60(3) > retry_delay_v60(2));
    }

    #[test]
    fn test_router_v60() {
        router_send_v60("peer1", &[1, 2, 3]);
    }

    #[test]
    fn test_stats_v60() {
        stats_record_v60(10.0);
        stats_record_v60(20.0);
        let avg = stats_avg_v60();
        assert!(avg > 0.0);
    }

    #[test]
    fn test_rle_v60() {
        let data = vec![5, 5, 5, 3, 3];
        let compressed = rle_compress_v60(&data);
        assert!(compressed.len() <= data.len() + 2); // RLE can be slightly larger
    }

    // ─── v0.70 Tests ───

    #[test]
    fn test_proto_v70() { assert_eq!(proto_version_v70(), "0.70.0"); }

    #[test]
    fn test_qos_v70() { assert_eq!(qos_level_v70(4), 4); assert_eq!(qos_level_v70(99), 4); }

    #[test]
    fn test_sign_v70() {
        let sig = frame_sign_v70(b"data", b"key");
        assert_eq!(sig.len(), 4);
        let sig2 = frame_sign_v70(b"data", b"key");
        assert_eq!(sig, sig2);
    }

    #[test]
    fn test_rate_v70() {
        assert!(rate_check_v70(0.0, 2.0));
        assert!(rate_check_v70(0.0, 2.0));
    }

    #[test]
    fn test_split_v70() {
        let chunk = split_stream_v70(&[1, 2, 3, 4], 2);
        assert_eq!(chunk.len(), 2);
    }

    #[test]
    fn test_watermark_v70() {
        assert_eq!(watermark_v70(50.0, 10.0, 90.0), "normal");
        assert_eq!(watermark_v70(95.0, 10.0, 90.0), "high");
    }

    #[test]
    fn test_reorder_v70() {
        reorder_push_v70(1, &[2]);
        reorder_push_v70(0, &[1]);
        let first = reorder_pop_v70();
        assert_eq!(first, vec![1]);
    }

    #[test]
    fn test_timeout_v70() {
        let expired = timeout_check_v70(0.0, 100.0);
        assert_eq!(expired, 0);
    }

    #[test]
    fn test_delta_v70() {
        let prev = vec![10, 20];
        let cur = vec![12, 22];
        let delta = delta_encode_v70(&prev, &cur);
        assert_ne!(delta, cur);
    }

    // ─── v0.75 Tests ───

    #[test]
    fn test_journal_v75() {
        journal_append_v75("test");
        assert!(journal_len_v75() > 0);
    }

    #[test]
    fn test_config_v75() {
        config_set_v75("key", "val");
        assert!(config_gen_v75() > 0);
    }

    #[test]
    fn test_dedup_v75() {
        assert!(!dedup_check_v75(b"unique_data_v75"));
        assert!(dedup_check_v75(b"unique_data_v75"));
    }

    #[test]
    fn test_circuit_v75() {
        assert_eq!(circuit_state_v75(), "closed");
    }

    #[test]
    fn test_bucket_v75() {
        assert!(bucket_take_v75(1.0));
    }

    #[test]
    fn test_batch_v75() {
        let count = batch_add_v75(&[1, 2, 3]);
        assert!(count > 0);
        let flushed = batch_flush_v75();
        assert!(flushed > 0);
    }

    #[test]
    fn test_chain_crc_v75() {
        let c1 = chain_crc_v75(b"hello");
        let c2 = chain_crc_v75(b"world");
        assert_ne!(c1, c2);
    }

    #[test]
    fn test_ping_v75() {
        ping_record_v75(100.0);
        ping_record_v75(50.0);
        assert!(ping_avg_v75() > 0.0);
    }

    #[test]
    fn test_circuit_fail_v75() {
        circuit_fail_v75();
    }

    // ─── v0.80 Tests ───

    #[test]
    fn test_abr_v80() { let q = abr_quality_v80(0.0, 10.0); assert!(q <= 4); }

    #[test]
    fn test_jitter_v80() { jitter_push_v80(&[1]); jitter_push_v80(&[2]); }

    #[test]
    fn test_frame_count_v80() { frame_tick_v80(); assert!(frame_count_v80() > 0); }

    #[test]
    fn test_event_v80() { event_emit_v80("test"); assert!(event_count_v80() > 0); }

    #[test]
    fn test_tee_v80() { assert_eq!(tee_count_v80(3), 3); }

    #[test]
    fn test_gain_v80() { let v = gain_apply_v80(50.0, 100.0); assert!(v > 40.0); }

    #[test]
    fn test_lossy_v80() { lossy_push_v80(&[1, 2]); let data = lossy_pop_v80(); assert!(!data.is_empty()); }

    #[test]
    fn test_header_v80() { let hdr = header_build_v80(42, 1, 0x80); assert_eq!(hdr.len(), 6); }

    #[test]
    fn test_session_v80() { session_start_v80(0.0); let d = session_duration_v80(5000.0); assert_eq!(d, 5000.0); }

    // ─── v0.90 Tests ───

    #[test]
    fn test_xor_v2_v90() {
        let enc = xor_v2_v90(b"hello", &[0xAA, 0xBB]);
        let dec = xor_v2_v90(&enc, &[0xAA, 0xBB]);
        assert_eq!(dec, b"hello");
    }

    #[test]
    fn test_channel_v90() { assert_eq!(channel_route_v90(1, &[1, 2]), 1); }

    #[test]
    fn test_ack_v90() { ack_send_v90(0); assert!(!ack_check_v90(0)); }

    #[test]
    fn test_pool_v90() { let buf = pool_alloc_v90(64); assert_eq!(buf.len(), 64); pool_free_v90(&buf); }

    #[test]
    fn test_bw_v90() { let e = bw_estimate_v90(1000, 100.0); assert!(e > 0.0); }

    #[test]
    fn test_mux_v90() { mux_push_v90(3, &[1]); mux_push_v90(1, &[2]); let d = mux_pop_v90(); assert_eq!(d, vec![1]); }

    #[test]
    fn test_nonce_v90() { let n1 = nonce_next_v90(); let n2 = nonce_next_v90(); assert!(n2 > n1); }

    #[test]
    fn test_validate_v90() { assert!(validate_seq_v90(0)); assert!(validate_seq_v90(1)); }

    #[test]
    fn test_retry_v90() { retry_push_v90(&[1, 2]); assert!(retry_count_v90() > 0); }

    // ─── v0.95 Tests ───

    #[test]
    fn test_lz4_v95() {
        let data = vec![0u8; 50];
        let compressed = lz4_compress_v95(&data);
        let decompressed = lz4_decompress_v95(&compressed);
        assert_eq!(decompressed, data);
    }

    #[test]
    fn test_telemetry_v95() { telemetry_emit_v95("fps", 60.0); assert!(telemetry_count_v95() > 0); }

    #[test]
    fn test_diff_v95() {
        let d = diff_frames_v95(&[10, 20], &[10, 25]);
        assert_eq!(d, vec![0, 13]); // 20^25 = 13
    }

    #[test]
    fn test_backoff_v95() { let d = backoff_next_v95(); assert!(d >= 100.0); }

    #[test]
    fn test_mirror_v95() { mirror_push_v95(&[1, 2]); let d = mirror_pop_v95(); assert_eq!(d, vec![1, 2]); }

    #[test]
    fn test_quota_v95() { assert!(quota_consume_v95(100)); }

    #[test]
    fn test_heartbeat_v95() { heartbeat_ping_v95(100.0); let e = heartbeat_elapsed_v95(110.0); assert_eq!(e, 10.0); }

    #[test]
    fn test_tag_v95() { assert!(tag_filter_v95("video")); assert!(!tag_filter_v95("unknown")); }

    #[test]
    fn test_mavg_v95() { mavg_push_v95(10.0); mavg_push_v95(20.0); assert!(mavg_get_v95() > 0.0); }

    // ─── v1.0 Tests ───

    #[test]
    fn test_pipeline_v100() { pipeline_add_v100("encode"); assert!(pipeline_count_v100() > 0); }

    #[test]
    fn test_proto_header_v100() { let h = proto_header_v100(1, 0x80, 42); assert_eq!(h.len(), 7); }

    #[test]
    fn test_splitter_v100() { splitter_push_v100(&[1, 2, 3, 4, 5], 2); let c = splitter_pop_v100(); assert!(!c.is_empty()); }

    #[test]
    fn test_cwnd_v100() { let w = cwnd_ack_v100(); assert!(w > 0.0); }

    #[test]
    fn test_stats_v100() { let s = stream_stats_v100(); assert!(s.contains("frames")); }

    #[test]
    fn test_ack_window_v100() { assert!(ack_window_v100(0)); }

    #[test]
    fn test_codec_v100() { codec_register_v100("webp"); let l = codec_list_v100(); assert!(l.contains("webp")); }

    #[test]
    fn test_flow_v100() { assert!(flow_credit_v100(1)); }

    #[test]
    fn test_version_v100() { let v = version_negotiate_v100("1.0"); assert_eq!(v, "1.0"); }
}




















