// ── Session journal record types ────────────────────────────────────────────

use crate::wal::error::WalError;

/// Record type discriminant for session start events.
pub const SESSION_RECORD_START: u8 = 0x01;
/// Record type discriminant for session signal events.
pub const SESSION_RECORD_SIGNAL: u8 = 0x02;
/// Record type discriminant for session close events.
pub const SESSION_RECORD_CLOSE: u8 = 0x03;

/// Versioned-record marker.
///
/// A record whose first payload byte equals this value carries a versioned,
/// checksummed frame (see [`SESSION_RECORD_VERSION_V2`]). Legacy (v1) records
/// begin directly with a [`SESSION_RECORD_START`] / `_SIGNAL` / `_CLOSE`
/// discriminant, so `0xFF` can never collide with a legacy record type and is
/// an unambiguous discriminator between the two layouts.
const SESSION_RECORD_VERSIONED: u8 = 0xFF;

/// Session-record format version 2: adds a trailing per-record BLAKE3 checksum.
///
/// V2 frames are laid out as
/// `[len: u32 LE][marker: 0xFF][version: 1][type: u8][payload][checksum: 8]`,
/// where `len` covers every byte after the length prefix (marker + version +
/// type + payload + checksum). The checksum is the first 8 bytes of the
/// BLAKE3 hash of `[marker, version, type] || payload` and is verified on
/// decode; a mismatch is treated as end-of-valid-data (the same clean stop as
/// a torn tail), never a panic.
///
/// Version 1 is the pre-checksum layout (`[len][type][payload]`) and is still
/// accepted on replay so existing journals recover without corruption.
pub const SESSION_RECORD_VERSION_V2: u8 = 2;

/// Length in bytes of the per-record checksum trailer (BLAKE3 prefix).
///
/// 8 bytes (64 bits) of BLAKE3 is ample to detect mid-record corruption /
/// torn writes; the session journal is low-frequency, so the trailer cost is
/// negligible and the goal is detection, not collision-resistant signing.
const SESSION_CHECKSUM_LEN: usize = 8;

/// Length of the per-record length prefix (`len: u32 LE`).
const SESSION_LEN_PREFIX_LEN: usize = 4;

/// Length of a v2 frame's in-`len` header before the typed payload:
/// `marker(1) + version(1) + type(1)`. A legacy v1 frame's header is just
/// `type(1)`, so its prefix is `SESSION_FRAME_PREFIX_LEN - 2`.
const SESSION_FRAME_PREFIX_LEN: usize = 3;

/// Bytes consumed by the v2 marker + version pair, skipped before the type byte.
const SESSION_MARKER_VERSION_LEN: usize = 2;

/// Fixed-size head of a Start payload before the variable-length string fields:
/// `session_id(8) + user_id(8) + started_at_ns(8)`.
const SESSION_START_FIXED_PREFIX: usize = 24;

/// Fixed-size head of a Signal payload before the variable-length string fields:
/// `session_id(8) + entity_id(8) + weight(8 f64) + ts_ns(8)`.
const SESSION_SIGNAL_FIXED_PREFIX: usize = 32;

/// Append an optional field as `[present: u8][payload]`.
///
/// `0x00` means absent (no payload follows); `0x01` means present, followed by
/// whatever `write_payload` appends. This is the single framing used by every
/// optional Signal field (annotation, session seqno, idempotency key), so the
/// `[flag][payload]` convention lives in exactly one place instead of being
/// hand-rolled per field. `write_payload` is fallible so an optional string
/// field (the annotation) can reject an over-length value via [`push_str_field`]
/// without being silently truncated.
fn push_optional<T>(
    buf: &mut Vec<u8>,
    value: Option<&T>,
    write_payload: impl FnOnce(&mut Vec<u8>, &T) -> Result<(), WalError>,
) -> Result<(), WalError> {
    if let Some(v) = value {
        buf.push(1u8);
        write_payload(buf, v)
    } else {
        buf.push(0u8);
        Ok(())
    }
}

/// Append a length-prefixed UTF-8 string field as `[len: u16 LE][bytes]`.
///
/// This is the single encode-side owner of the `[len: u16][bytes]` framing,
/// mirroring the decode-side [`read_str_field`]. Hand-rolling
/// `(s.len() as u16).to_le_bytes()` per field (as the four call sites used to)
/// is exactly the bug this fixes: a `len() > u16::MAX` silently *wraps* the
/// 16-bit length while the full byte payload is still appended, producing a
/// frame whose own checksum passes but that `read_str_field` reads back
/// truncated — silently dropping the over-length field's tail AND misframing
/// every later record in the journal. Encoding must never produce a record it
/// cannot faithfully decode, so we reject the over-length field here with a
/// typed error rather than casting.
///
/// # Errors
///
/// Returns [`WalError::Corruption`] if `s.len() > u16::MAX` (the largest length
/// the 16-bit prefix can represent).
fn push_str_field(buf: &mut Vec<u8>, field: &str, value: &str) -> Result<(), WalError> {
    let len = u16::try_from(value.len()).map_err(|_| WalError::Corruption {
        message: format!(
            "session journal {field} field is {} bytes, exceeds the u16 length-prefix maximum of {}",
            value.len(),
            u16::MAX
        ),
    })?;
    buf.extend_from_slice(&len.to_le_bytes());
    buf.extend_from_slice(value.as_bytes());
    Ok(())
}

/// Compute the per-record checksum over the framed record body.
///
/// Input is the in-`len` body excluding the trailing checksum, i.e.
/// `[marker, version, type] || payload`. Returns the first
/// [`SESSION_CHECKSUM_LEN`] bytes of the BLAKE3 hash.
fn session_record_checksum(body: &[u8]) -> [u8; SESSION_CHECKSUM_LEN] {
    let hash = blake3::hash(body);
    let mut out = [0u8; SESSION_CHECKSUM_LEN];
    out.copy_from_slice(&hash.as_bytes()[..SESSION_CHECKSUM_LEN]);
    out
}

/// Monotonic sequence number for session writes.
///
/// Incremented once per session write operation (preference signal,
/// annotation, search query). Used by the receiver to enforce idempotent
/// replay: a write with seqno <= hwm is a no-op.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct SessionSeqNo(pub u64);

impl SessionSeqNo {
    pub const ZERO: Self = Self(0);
    #[must_use]
    /// Saturates at `u64::MAX` (~1.8e19), which is unreachable in practice.
    pub const fn next(self) -> Self {
        Self(self.0.saturating_add(1))
    }
}

impl std::fmt::Display for SessionSeqNo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ssn:{}", self.0)
    }
}

/// A session event decoded from the session journal.
///
/// These events are stored in a separate append-only file (`sessions.log`)
/// alongside the signal WAL. They are used to restore active sessions on
/// crash recovery.
#[derive(Debug, Clone, PartialEq)]
pub enum SessionWalEvent {
    /// A session was started.
    Start {
        session_id: u64,
        user_id: u64,
        started_at_ns: u64,
        agent_id: String,
        policy_name: String,
    },
    /// A signal was written within a session.
    Signal {
        session_id: u64,
        entity_id: u64,
        /// Stored at full f64 precision to match the in-memory decay path; a
        /// session restored from this journal reconstructs the identical score
        /// the live session held (no f32 narrowing on the WAL round-trip).
        weight: f64,
        ts_ns: u64,
        signal_name: String,
        annotation: Option<String>,
        // m8p4 extensions (default None for legacy events)
        session_seqno: Option<SessionSeqNo>,
        idempotency_key: Option<u128>,
    },
    /// A session was closed.
    Close { session_id: u64 },
}

/// Encode a session event to bytes for the session journal.
///
/// Format (version 2): `[len: u32 LE][marker: 0xFF][version: u8][type: u8][payload bytes][checksum: 8 bytes]`
///
/// `len` is the length of everything after the length prefix
/// (`marker + version + type + payload + checksum`). `checksum` is the first
/// [`SESSION_CHECKSUM_LEN`] bytes of the BLAKE3 hash of
/// `[marker, version, type] || payload`, used to detect mid-record corruption
/// on replay. The pre-checksum version-1 layout (`[len][type][payload]`) is
/// still decoded by [`decode_session_events`] so old journals recover cleanly.
///
/// **Start payload**: `[session_id: u64 LE][user_id: u64 LE][started_at_ns: u64 LE]`
///   `[agent_id_len: u16 LE][agent_id: bytes][policy_name_len: u16 LE][policy_name: bytes]`
///
/// **Signal payload**: `[session_id: u64 LE][entity_id: u64 LE][weight: f64 LE][ts_ns: u64 LE]`
///   `[signal_name_len: u16 LE][signal_name: bytes][has_annotation: u8]`
///   `[if has_annotation: annotation_len: u16 LE, annotation: bytes]`
///
/// **Close payload**: `[session_id: u64 LE]`
///
/// # Errors
///
/// Returns [`WalError::Corruption`] if any variable-length string field
/// (`agent_id`, `policy_name`, `signal_name`, or `annotation`) is longer than
/// `u16::MAX` bytes. The length prefix is a `u16`, so an over-length field
/// could only be encoded by truncating its length count — producing a frame
/// whose checksum passes but that decodes back truncated and misframes every
/// later record. The format layer is self-defending: it refuses to emit a
/// record it cannot faithfully decode, regardless of what the write API let
/// through upstream.
pub fn encode_session_event(event: &SessionWalEvent) -> Result<Vec<u8>, WalError> {
    // Encode the body (marker + version + type + payload) first, then prepend
    // the length prefix and append the per-record checksum trailer.
    let mut payload = Vec::new();
    payload.push(SESSION_RECORD_VERSIONED);
    payload.push(SESSION_RECORD_VERSION_V2);
    match event {
        SessionWalEvent::Start {
            session_id,
            user_id,
            started_at_ns,
            agent_id,
            policy_name,
        } => {
            payload.push(SESSION_RECORD_START);
            payload.extend_from_slice(&session_id.to_le_bytes());
            payload.extend_from_slice(&user_id.to_le_bytes());
            payload.extend_from_slice(&started_at_ns.to_le_bytes());
            push_str_field(&mut payload, "agent_id", agent_id)?;
            push_str_field(&mut payload, "policy_name", policy_name)?;
        }
        SessionWalEvent::Signal {
            session_id,
            entity_id,
            weight,
            ts_ns,
            signal_name,
            annotation,
            session_seqno,
            idempotency_key,
        } => {
            payload.push(SESSION_RECORD_SIGNAL);
            payload.extend_from_slice(&session_id.to_le_bytes());
            payload.extend_from_slice(&entity_id.to_le_bytes());
            payload.extend_from_slice(&weight.to_le_bytes());
            payload.extend_from_slice(&ts_ns.to_le_bytes());
            push_str_field(&mut payload, "signal_name", signal_name)?;
            push_optional(&mut payload, annotation.as_ref(), |buf, ann| {
                push_str_field(buf, "annotation", ann)
            })?;
            // m8p4: session_seqno (optional u64)
            push_optional(&mut payload, session_seqno.as_ref(), |buf, seqno| {
                buf.extend_from_slice(&seqno.0.to_le_bytes());
                Ok(())
            })?;
            // m8p4: idempotency_key (optional u128)
            push_optional(&mut payload, idempotency_key.as_ref(), |buf, key| {
                buf.extend_from_slice(&key.to_le_bytes());
                Ok(())
            })?;
        }
        SessionWalEvent::Close { session_id } => {
            payload.push(SESSION_RECORD_CLOSE);
            payload.extend_from_slice(&session_id.to_le_bytes());
        }
    }

    // Per-record checksum over the body (marker + version + type + payload).
    let checksum = session_record_checksum(&payload);
    let len = (payload.len() + SESSION_CHECKSUM_LEN) as u32;
    let mut buf = Vec::with_capacity(SESSION_LEN_PREFIX_LEN + payload.len() + SESSION_CHECKSUM_LEN);
    buf.extend_from_slice(&len.to_le_bytes());
    buf.extend(payload);
    buf.extend_from_slice(&checksum);
    Ok(buf)
}

/// Decode all session events from a session journal file's contents.
///
/// Stops at the first truncated, checksum-mismatched, or malformed record.
/// This is the correct behavior for crash recovery: a torn write or
/// mid-record corruption at the end of the file is simply ignored.
///
/// Both the version-2 (checksummed) and legacy version-1 (pre-checksum)
/// record layouts are accepted, so journals written before the checksum
/// trailer existed still recover cleanly.
#[must_use]
pub fn decode_session_events(bytes: &[u8]) -> Vec<SessionWalEvent> {
    decode_session_events_with_diagnostics(bytes).events
}

/// Outcome of decoding a session journal, including a corruption signal.
///
/// `corruption_detected` is set when a checksum-mismatched (or otherwise
/// malformed) record forced the decode to stop early. It distinguishes a
/// clean end-of-file from a stop caused by detected corruption, so operators
/// can observe session-journal corruption events (see
/// [`crate::wal::diagnostics`]).
#[derive(Debug, Clone)]
pub struct SessionDecodeOutcome {
    /// The valid events decoded before the stop point.
    pub events: Vec<SessionWalEvent>,
    /// Number of records that failed checksum verification or were malformed
    /// (not counting a clean torn-tail truncation, which is normal).
    pub corruption_count: u64,
    /// Whether decoding stopped because of detected corruption rather than a
    /// clean end of the byte stream.
    pub corruption_detected: bool,
}

/// Decode session events, reporting any detected corruption alongside them.
///
/// Behaves identically to [`decode_session_events`] for the returned event
/// list, but additionally reports a corruption count. A clean torn tail (a
/// truncated final frame, normal after a crash mid-append) is NOT counted as
/// corruption; a checksum mismatch on an otherwise complete frame IS, because
/// it indicates the bytes on disk were silently mangled.
#[must_use]
pub fn decode_session_events_with_diagnostics(bytes: &[u8]) -> SessionDecodeOutcome {
    let mut events = Vec::new();
    let mut corruption_count = 0u64;
    let mut corruption_detected = false;
    let mut pos = 0;

    while pos + SESSION_LEN_PREFIX_LEN <= bytes.len() {
        let record_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += SESSION_LEN_PREFIX_LEN;

        if pos + record_len > bytes.len() || record_len == 0 {
            // Truncated or zero-length record -- clean torn tail, stop.
            break;
        }

        let record_end = pos + record_len;

        // Determine the record layout from the first body byte.
        if bytes[pos] == SESSION_RECORD_VERSIONED {
            // Version-2 (checksummed) frame: [marker][version][type][payload][checksum].
            // marker(1) + version(1) + type(1) + checksum(SESSION_CHECKSUM_LEN)
            // is the minimum a v2 frame can occupy.
            if record_len < SESSION_FRAME_PREFIX_LEN + SESSION_CHECKSUM_LEN {
                // Too short to be a well-formed v2 frame -- treat as corruption.
                corruption_count += 1;
                corruption_detected = true;
                break;
            }
            let body_end = record_end - SESSION_CHECKSUM_LEN;
            let body = &bytes[pos..body_end];
            let stored = &bytes[body_end..record_end];
            if session_record_checksum(body) != stored {
                // Checksum mismatch -- mid-record corruption. Stop cleanly,
                // exactly like the torn-tail path, and count it so operators
                // can observe the event. Never panic.
                corruption_count += 1;
                corruption_detected = true;
                break;
            }
            // Skip marker + version; decode the typed body. The checksum already
            // passed, so an unknown record type is intentional forward-compat.
            pos += SESSION_MARKER_VERSION_LEN;
            let record_type = bytes[pos];
            pos += 1;
            match decode_typed_record(bytes, &mut pos, body_end, record_type, true, &mut events) {
                RecordOutcome::Decoded | RecordOutcome::Skipped => {}
                RecordOutcome::Malformed => {
                    // The BLAKE3 trailer already verified these exact bytes, so
                    // they were NOT silently mangled on disk. A known type whose
                    // body is too short to fully decode is therefore a structural
                    // mismatch from a torn/forward-compat layout, not the
                    // bytes-silently-mangled corruption that `corruption_detected`
                    // exists to flag. Stop cleanly, exactly like a torn tail, and
                    // do NOT count it as corruption.
                    break;
                }
            }
        } else {
            // Legacy version-1 frame: [type][payload], no checksum. With no
            // integrity check, an unknown record type cannot be distinguished
            // from a corrupted type byte, so it is treated as corruption.
            let record_type = bytes[pos];
            pos += 1;
            match decode_typed_record(bytes, &mut pos, record_end, record_type, false, &mut events)
            {
                RecordOutcome::Decoded | RecordOutcome::Skipped => {}
                RecordOutcome::Malformed => {
                    corruption_count += 1;
                    corruption_detected = true;
                    break;
                }
            }
        }

        // Ensure pos is at the expected record boundary.
        pos = record_end;
    }

    SessionDecodeOutcome {
        events,
        corruption_count,
        corruption_detected,
    }
}

/// Outcome of decoding one typed session record body.
///
/// The caller maps this onto the [`SessionDecodeOutcome`] corruption contract:
/// a `Malformed` body in a *checksum-validated* (v2) frame is a clean stop (the
/// bytes were proven intact, so the mismatch is a torn/forward-compat layout,
/// not silent on-disk mangling), whereas the same `Malformed` in a legacy frame
/// is counted as corruption.
enum RecordOutcome {
    /// A well-formed record was decoded and pushed onto the event list.
    Decoded,
    /// A forward-compatible record was intentionally skipped (v2 unknown type).
    Skipped,
    /// The body was structurally invalid for its (known) type and decoding
    /// should stop. The caller decides whether to flag it as corruption.
    Malformed,
}

/// Decode a single typed record body (`type` already consumed) into `events`.
///
/// `end` is the exclusive boundary of the typed payload (for a v2 frame this
/// is the start of the checksum trailer; for a legacy frame it is the record
/// end). `checksummed` is `true` for v2 frames whose BLAKE3 trailer already
/// verified the type byte. Returns [`RecordOutcome::Malformed`] if the body was
/// malformed (or, for a legacy frame, the type byte was unrecognised) and
/// decoding should stop.
fn decode_typed_record(
    bytes: &[u8],
    pos: &mut usize,
    end: usize,
    record_type: u8,
    checksummed: bool,
    events: &mut Vec<SessionWalEvent>,
) -> RecordOutcome {
    match record_type {
        SESSION_RECORD_START => match decode_start_record(bytes, pos, end) {
            Some(event) => {
                events.push(event);
                RecordOutcome::Decoded
            }
            None => RecordOutcome::Malformed,
        },
        SESSION_RECORD_SIGNAL => match decode_signal_record(bytes, pos, end) {
            Some(event) => {
                events.push(event);
                RecordOutcome::Decoded
            }
            None => RecordOutcome::Malformed,
        },
        SESSION_RECORD_CLOSE => {
            if *pos + 8 > end {
                return RecordOutcome::Malformed;
            }
            let session_id = read_u64_le(bytes, pos);
            events.push(SessionWalEvent::Close { session_id });
            RecordOutcome::Decoded
        }
        _ if checksummed => {
            // v2: the checksum already verified the type byte, so an unknown
            // type is an intentional forward-compatible record from a newer
            // writer. The caller advances `pos` to the record boundary, so it
            // is skipped without aborting replay or flagging corruption.
            RecordOutcome::Skipped
        }
        _ => {
            // Legacy v1 (un-checksummed): an unknown type byte is
            // indistinguishable from a flipped/corrupted discriminant, so it is
            // treated as corruption (stop and count it) rather than silently
            // dropped as forward-compat.
            RecordOutcome::Malformed
        }
    }
}

/// Helper: read a little-endian u64 from `bytes` at `*pos`, advancing `*pos`.
fn read_u64_le(bytes: &[u8], pos: &mut usize) -> u64 {
    let v = u64::from_le_bytes([
        bytes[*pos],
        bytes[*pos + 1],
        bytes[*pos + 2],
        bytes[*pos + 3],
        bytes[*pos + 4],
        bytes[*pos + 5],
        bytes[*pos + 6],
        bytes[*pos + 7],
    ]);
    *pos += 8;
    v
}

/// Helper: read a little-endian u16 from `bytes` at `*pos`, advancing `*pos`.
fn read_u16_le(bytes: &[u8], pos: &mut usize) -> u16 {
    let v = u16::from_le_bytes([bytes[*pos], bytes[*pos + 1]]);
    *pos += 2;
    v
}

/// Read a length-prefixed UTF-8 string field at `*pos`, advancing `*pos`.
///
/// Layout is `[len: u16 LE][bytes]`. Returns `None` (treated as a malformed
/// record by the caller) if the field runs past `end` or the bytes are not
/// valid UTF-8 — a bit-flip in a legacy (un-checksummed) frame's string field
/// is detected as corruption rather than silently rewritten to U+FFFD.
fn read_str_field(bytes: &[u8], pos: &mut usize, end: usize) -> Option<String> {
    if *pos + 2 > end {
        return None;
    }
    let len = read_u16_le(bytes, pos) as usize;
    if *pos + len > end {
        return None;
    }
    let s = std::str::from_utf8(&bytes[*pos..*pos + len])
        .ok()?
        .to_owned();
    *pos += len;
    Some(s)
}

/// Decode a Start record from the payload region.
fn decode_start_record(bytes: &[u8], pos: &mut usize, end: usize) -> Option<SessionWalEvent> {
    if *pos + SESSION_START_FIXED_PREFIX > end {
        return None;
    }
    let session_id = read_u64_le(bytes, pos);
    let user_id = read_u64_le(bytes, pos);
    let started_at_ns = read_u64_le(bytes, pos);

    let agent_id = read_str_field(bytes, pos, end)?;
    let policy_name = read_str_field(bytes, pos, end)?;

    Some(SessionWalEvent::Start {
        session_id,
        user_id,
        started_at_ns,
        agent_id,
        policy_name,
    })
}

/// Read a little-endian f64 from `bytes` at `*pos`, advancing `*pos`.
fn read_f64_le(bytes: &[u8], pos: &mut usize) -> f64 {
    let v = f64::from_le_bytes([
        bytes[*pos],
        bytes[*pos + 1],
        bytes[*pos + 2],
        bytes[*pos + 3],
        bytes[*pos + 4],
        bytes[*pos + 5],
        bytes[*pos + 6],
        bytes[*pos + 7],
    ]);
    *pos += 8;
    v
}

/// Decode a Signal record from the payload region.
fn decode_signal_record(bytes: &[u8], pos: &mut usize, end: usize) -> Option<SessionWalEvent> {
    // session_id(8) + entity_id(8) + weight(8 f64) + ts_ns(8) = SESSION_SIGNAL_FIXED_PREFIX
    if *pos + SESSION_SIGNAL_FIXED_PREFIX > end {
        return None;
    }
    let session_id = read_u64_le(bytes, pos);
    let entity_id = read_u64_le(bytes, pos);
    let weight = read_f64_le(bytes, pos);
    let ts_ns = read_u64_le(bytes, pos);

    let signal_name = read_str_field(bytes, pos, end)?;

    if *pos + 1 > end {
        return None;
    }
    let has_annotation = bytes[*pos] != 0;
    *pos += 1;

    let annotation = if has_annotation {
        Some(read_str_field(bytes, pos, end)?)
    } else {
        None
    };

    // m8p4: session_seqno (backward-compatible — absent in legacy records).
    //
    // An absent flag byte (`*pos == end`) means a legacy record that predates
    // this field: that is `None`, not malformed. But if the flag byte IS present
    // and SET (`has_seqno != 0`) while the 8-byte value runs past `end`, the
    // frame is structurally broken — the writer always emits the full value when
    // it sets the flag. We return `None` (a `Malformed` outcome to the caller)
    // rather than silently decoding the present-but-short field to `None`, which
    // would drop a field the encoder claimed was there. A genuine writer can
    // never produce this; it is only reachable via a hand-crafted frame with a
    // recomputed-valid checksum.
    let session_seqno = if *pos < end {
        let has_seqno = bytes[*pos];
        *pos += 1;
        if has_seqno != 0 {
            if *pos + 8 > end {
                return None;
            }
            let v = read_u64_le(bytes, pos);
            Some(SessionSeqNo(v))
        } else {
            None
        }
    } else {
        None
    };

    // m8p4: idempotency_key (backward-compatible — absent in legacy records).
    // Same contract as `session_seqno`: a present-but-short value is structural
    // corruption, not a silently-dropped `None`.
    let idempotency_key = if *pos < end {
        let has_key = bytes[*pos];
        *pos += 1;
        if has_key != 0 {
            if *pos + 16 > end {
                return None;
            }
            let v = u128::from_le_bytes([
                bytes[*pos],
                bytes[*pos + 1],
                bytes[*pos + 2],
                bytes[*pos + 3],
                bytes[*pos + 4],
                bytes[*pos + 5],
                bytes[*pos + 6],
                bytes[*pos + 7],
                bytes[*pos + 8],
                bytes[*pos + 9],
                bytes[*pos + 10],
                bytes[*pos + 11],
                bytes[*pos + 12],
                bytes[*pos + 13],
                bytes[*pos + 14],
                bytes[*pos + 15],
            ]);
            *pos += 16;
            Some(v)
        } else {
            None
        }
    } else {
        None
    };

    Some(SessionWalEvent::Signal {
        session_id,
        entity_id,
        weight,
        ts_ns,
        signal_name,
        annotation,
        session_seqno,
        idempotency_key,
    })
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)]
mod tests {
    use super::*;

    /// Encode an event, unwrapping the (now fallible) encoder. Every fixed test
    /// event has short string fields, so the encode cannot fail; centralizing
    /// the unwrap keeps the round-trip assertions readable. Tests that
    /// deliberately exercise the over-length rejection path call
    /// `encode_session_event` directly and assert on the `Result`.
    fn enc(event: &SessionWalEvent) -> Vec<u8> {
        encode_session_event(event).expect("fixed test events encode losslessly")
    }

    #[test]
    fn session_start_roundtrip() {
        let event = SessionWalEvent::Start {
            session_id: 42,
            user_id: 100,
            started_at_ns: 1_000_000_000,
            agent_id: "test-agent".to_string(),
            policy_name: "default_policy".to_string(),
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_signal_roundtrip_with_annotation() {
        let event = SessionWalEvent::Signal {
            session_id: 7,
            entity_id: 999,
            weight: 1.5,
            ts_ns: 2_000_000_000,
            signal_name: "reward".to_string(),
            annotation: Some("jazz fusion".to_string()),
            session_seqno: None,
            idempotency_key: None,
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_signal_roundtrip_without_annotation() {
        let event = SessionWalEvent::Signal {
            session_id: 7,
            entity_id: 999,
            weight: 1.5,
            ts_ns: 2_000_000_000,
            signal_name: "view".to_string(),
            annotation: None,
            session_seqno: None,
            idempotency_key: None,
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_close_roundtrip() {
        let event = SessionWalEvent::Close { session_id: 42 };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_multiple_events_roundtrip() {
        let events = vec![
            SessionWalEvent::Start {
                session_id: 1,
                user_id: 10,
                started_at_ns: 100,
                agent_id: "agent-a".to_string(),
                policy_name: "policy-1".to_string(),
            },
            SessionWalEvent::Signal {
                session_id: 1,
                entity_id: 42,
                weight: 1.0,
                ts_ns: 200,
                signal_name: "view".to_string(),
                annotation: None,
                session_seqno: None,
                idempotency_key: None,
            },
            SessionWalEvent::Signal {
                session_id: 1,
                entity_id: 43,
                weight: 2.0,
                ts_ns: 300,
                signal_name: "reward".to_string(),
                annotation: Some("good content".to_string()),
                session_seqno: None,
                idempotency_key: None,
            },
            SessionWalEvent::Close { session_id: 1 },
        ];

        let mut all_bytes = Vec::new();
        for event in &events {
            all_bytes.extend(enc(event));
        }

        let decoded = decode_session_events(&all_bytes);
        assert_eq!(decoded.len(), events.len());
        for (orig, dec) in events.iter().zip(decoded.iter()) {
            assert_eq!(orig, dec);
        }
    }

    #[test]
    fn session_decode_truncated_stops_cleanly() {
        let event = SessionWalEvent::Start {
            session_id: 1,
            user_id: 10,
            started_at_ns: 100,
            agent_id: "agent".to_string(),
            policy_name: "policy".to_string(),
        };
        let encoded = enc(&event);

        // Truncate the record mid-way.
        let truncated = &encoded[..encoded.len() / 2];
        let decoded = decode_session_events(truncated);
        assert!(decoded.is_empty(), "truncated record should be skipped");
    }

    #[test]
    fn session_decode_partial_second_record_stops() {
        let e1 = SessionWalEvent::Close { session_id: 1 };
        let e2 = SessionWalEvent::Start {
            session_id: 2,
            user_id: 20,
            started_at_ns: 200,
            agent_id: "agent".to_string(),
            policy_name: "policy".to_string(),
        };
        let mut all_bytes = enc(&e1);
        let e2_bytes = enc(&e2);
        // Add partial second record.
        all_bytes.extend_from_slice(&e2_bytes[..e2_bytes.len() / 2]);

        let decoded = decode_session_events(&all_bytes);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], e1);
    }

    #[test]
    fn session_decode_empty_bytes() {
        let decoded = decode_session_events(&[]);
        assert!(decoded.is_empty());
    }

    #[test]
    fn session_seqno_ordering() {
        assert!(SessionSeqNo(1) > SessionSeqNo(0));
        assert_eq!(SessionSeqNo(5).next(), SessionSeqNo(6));
        assert_eq!(SessionSeqNo::ZERO, SessionSeqNo(0));
    }

    #[test]
    fn session_signal_with_seqno_roundtrip() {
        let event = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 42,
            weight: 1.0,
            ts_ns: 1000,
            signal_name: "view".to_string(),
            annotation: None,
            session_seqno: Some(SessionSeqNo(7)),
            idempotency_key: Some(0xDEAD_BEEF_CAFE_BABE_u128),
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_signal_legacy_roundtrip_no_seqno() {
        // Legacy events (seqno=None) decode correctly.
        let event = SessionWalEvent::Signal {
            session_id: 2,
            entity_id: 99,
            weight: 2.0,
            ts_ns: 2000,
            signal_name: "like".to_string(),
            annotation: None,
            session_seqno: None,
            idempotency_key: None,
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded[0], event);
    }

    // ── Checksum (version 2) integrity ──────────────────────────────────────

    /// Re-encode an event in the legacy version-1 layout (no marker, no
    /// checksum) so the backward-compat decode path can be exercised. Mirrors
    /// the pre-checksum framing: `[len: u32 LE][type: u8][payload bytes]`.
    fn encode_session_event_v1_legacy(event: &SessionWalEvent) -> Vec<u8> {
        // Encode a v2 record, then strip the marker/version prefix and the
        // checksum trailer to reconstruct the exact v1 byte layout.
        let v2 = enc(event);
        // v2 body starts after the length prefix with [marker, version, type, ...].
        let body = &v2[SESSION_LEN_PREFIX_LEN..v2.len() - SESSION_CHECKSUM_LEN];
        // Drop marker(1) + version(1) to leave [type, payload...].
        let legacy_body = &body[SESSION_MARKER_VERSION_LEN..];
        let len = legacy_body.len() as u32;
        let mut buf = Vec::with_capacity(SESSION_LEN_PREFIX_LEN + legacy_body.len());
        buf.extend_from_slice(&len.to_le_bytes());
        buf.extend_from_slice(legacy_body);
        buf
    }

    #[test]
    fn session_v2_record_has_checksum_trailer() {
        let event = SessionWalEvent::Close { session_id: 42 };
        let encoded = enc(&event);
        // len(4) + marker(1) + version(1) + type(1) + session_id(8) + checksum(8)
        assert_eq!(
            encoded.len(),
            SESSION_LEN_PREFIX_LEN + SESSION_FRAME_PREFIX_LEN + 8 + SESSION_CHECKSUM_LEN
        );
        assert_eq!(encoded[4], SESSION_RECORD_VERSIONED);
        assert_eq!(encoded[5], SESSION_RECORD_VERSION_V2);
        assert_eq!(encoded[6], SESSION_RECORD_CLOSE);
    }

    #[test]
    fn session_legacy_v1_record_decodes() {
        // A record written before the checksum trailer existed must still
        // recover cleanly (no marker byte, no checksum).
        let event = SessionWalEvent::Signal {
            session_id: 5,
            entity_id: 77,
            weight: 3.5,
            ts_ns: 4000,
            signal_name: "reward".to_string(),
            annotation: Some("legacy annotation".to_string()),
            session_seqno: Some(SessionSeqNo(9)),
            idempotency_key: Some(0x1234_5678_9ABC_DEF0_u128),
        };
        let legacy = encode_session_event_v1_legacy(&event);
        let decoded = decode_session_events(&legacy);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    #[test]
    fn session_mixed_legacy_and_v2_decode() {
        // A journal that contains a legacy record followed by a v2 record
        // (e.g. written across a version upgrade) decodes both.
        let legacy_event = SessionWalEvent::Start {
            session_id: 1,
            user_id: 10,
            started_at_ns: 100,
            agent_id: "legacy-agent".to_string(),
            policy_name: "p".to_string(),
        };
        let v2_event = SessionWalEvent::Close { session_id: 1 };

        let mut bytes = encode_session_event_v1_legacy(&legacy_event);
        bytes.extend(enc(&v2_event));

        let decoded = decode_session_events(&bytes);
        assert_eq!(decoded.len(), 2);
        assert_eq!(decoded[0], legacy_event);
        assert_eq!(decoded[1], v2_event);
    }

    #[test]
    fn session_corrupted_record_detected_and_stops_cleanly() {
        // Two valid records; flip a payload byte in the SECOND so its
        // checksum no longer matches. Decoding must keep the first record,
        // drop the corrupted one, stop cleanly (no panic), and flag it.
        let e1 = SessionWalEvent::Start {
            session_id: 1,
            user_id: 10,
            started_at_ns: 100,
            agent_id: "agent".to_string(),
            policy_name: "policy".to_string(),
        };
        let e2 = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 42,
            weight: 1.0,
            ts_ns: 200,
            signal_name: "view".to_string(),
            annotation: None,
            session_seqno: None,
            idempotency_key: None,
        };

        let r1 = enc(&e1);
        let mut r2 = enc(&e2);
        // Corrupt the first payload byte of r2, just past the length prefix and
        // the marker/version/type frame header.
        let flip = SESSION_LEN_PREFIX_LEN + SESSION_FRAME_PREFIX_LEN;
        r2[flip] ^= 0xFF;

        let mut bytes = r1;
        bytes.extend(r2);

        let outcome = decode_session_events_with_diagnostics(&bytes);
        assert_eq!(outcome.events.len(), 1, "only the intact record survives");
        assert_eq!(outcome.events[0], e1);
        assert!(outcome.corruption_detected, "mismatch must be flagged");
        assert_eq!(outcome.corruption_count, 1);

        // The infallible front-door API returns the same survivors.
        assert_eq!(decode_session_events(&bytes), vec![e1]);
    }

    #[test]
    fn session_corrupted_checksum_trailer_detected() {
        // Mangle only the checksum trailer (not the body): still detected.
        let event = SessionWalEvent::Close { session_id: 7 };
        let mut encoded = enc(&event);
        let last = encoded.len() - 1;
        encoded[last] ^= 0x01;

        let outcome = decode_session_events_with_diagnostics(&encoded);
        assert!(outcome.events.is_empty());
        assert!(outcome.corruption_detected);
        assert_eq!(outcome.corruption_count, 1);
    }

    #[test]
    fn session_clean_torn_tail_is_not_corruption() {
        // A truncated final frame is normal after a crash mid-append: it must
        // NOT be counted as corruption.
        let event = SessionWalEvent::Start {
            session_id: 1,
            user_id: 10,
            started_at_ns: 100,
            agent_id: "agent".to_string(),
            policy_name: "policy".to_string(),
        };
        let encoded = enc(&event);
        let truncated = &encoded[..encoded.len() - 3];

        let outcome = decode_session_events_with_diagnostics(truncated);
        assert!(outcome.events.is_empty());
        assert!(
            !outcome.corruption_detected,
            "a torn tail is not corruption"
        );
        assert_eq!(outcome.corruption_count, 0);
    }

    #[test]
    fn session_intact_journal_reports_no_corruption() {
        let events = vec![
            SessionWalEvent::Start {
                session_id: 1,
                user_id: 10,
                started_at_ns: 100,
                agent_id: "agent-a".to_string(),
                policy_name: "policy-1".to_string(),
            },
            SessionWalEvent::Signal {
                session_id: 1,
                entity_id: 42,
                weight: 1.0,
                ts_ns: 200,
                signal_name: "view".to_string(),
                annotation: Some("note".to_string()),
                session_seqno: Some(SessionSeqNo(3)),
                idempotency_key: Some(7),
            },
            SessionWalEvent::Close { session_id: 1 },
        ];
        let mut bytes = Vec::new();
        for e in &events {
            bytes.extend(enc(e));
        }
        let outcome = decode_session_events_with_diagnostics(&bytes);
        assert_eq!(outcome.events, events);
        assert!(!outcome.corruption_detected);
        assert_eq!(outcome.corruption_count, 0);
    }

    #[test]
    #[allow(clippy::float_cmp)] // exact round-trip is precisely what this asserts
    fn session_signal_weight_f64_precision_roundtrips() {
        // A weight that is not exactly representable as f32 must round-trip
        // through the journal unchanged (the WAL path no longer narrows to f32).
        let w = 0.123_456_789_012_345_67_f64;
        assert_ne!(
            w,
            f64::from(w as f32),
            "test weight must lose precision under f32 narrowing"
        );
        let event = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 2,
            weight: w,
            ts_ns: 3,
            signal_name: "reward".to_string(),
            annotation: None,
            session_seqno: None,
            idempotency_key: None,
        };
        let encoded = enc(&event);
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            SessionWalEvent::Signal { weight, .. } => {
                assert_eq!(*weight, w, "f64 weight must survive the round-trip exactly");
            }
            other => panic!("expected Signal, got {other:?}"),
        }
    }

    #[test]
    fn session_legacy_unknown_type_is_corruption() {
        // In a legacy (un-checksummed) frame, an unknown/corrupted type byte
        // cannot be distinguished from a forward-compat record, so it must be
        // flagged as corruption rather than silently dropped.
        let event = SessionWalEvent::Close { session_id: 7 };
        let mut legacy = encode_session_event_v1_legacy(&event);
        // The type byte is immediately after the 4-byte length prefix.
        legacy[SESSION_LEN_PREFIX_LEN] = 0x42; // not START/SIGNAL/CLOSE
        let outcome = decode_session_events_with_diagnostics(&legacy);
        assert!(outcome.events.is_empty());
        assert!(
            outcome.corruption_detected,
            "legacy unknown type must count as corruption"
        );
        assert_eq!(outcome.corruption_count, 1);
    }

    #[test]
    fn session_v2_unknown_type_is_forward_compat() {
        // In a v2 frame the checksum verified the type byte, so an unknown type
        // is an intentional forward-compatible record: skip it, no corruption.
        // Hand-build a v2 frame with type 0x42 and a valid checksum.
        let mut payload = vec![SESSION_RECORD_VERSIONED, SESSION_RECORD_VERSION_V2, 0x42];
        payload.extend_from_slice(&7u64.to_le_bytes()); // arbitrary body
        let checksum = session_record_checksum(&payload);
        let len = (payload.len() + SESSION_CHECKSUM_LEN) as u32;
        let mut frame = Vec::new();
        frame.extend_from_slice(&len.to_le_bytes());
        frame.extend_from_slice(&payload);
        frame.extend_from_slice(&checksum);
        // Follow it with a real Close so we can prove decoding continued.
        frame.extend(enc(&SessionWalEvent::Close { session_id: 9 }));

        let outcome = decode_session_events_with_diagnostics(&frame);
        assert!(
            !outcome.corruption_detected,
            "v2 unknown type is forward-compat, not corruption"
        );
        assert_eq!(outcome.corruption_count, 0);
        assert_eq!(
            outcome.events,
            vec![SessionWalEvent::Close { session_id: 9 }]
        );
    }

    #[test]
    fn session_v2_known_type_short_body_is_clean_stop() {
        // A v2 frame whose BLAKE3 trailer is valid (bytes proven intact) but
        // whose KNOWN type (Close) has a body too short for its fixed fields is
        // a torn/forward-compat layout, NOT silently-mangled corruption. It must
        // stop decoding cleanly without flagging corruption — distinct from the
        // checksum-mismatch path which IS corruption.
        let mut payload = vec![
            SESSION_RECORD_VERSIONED,
            SESSION_RECORD_VERSION_V2,
            SESSION_RECORD_CLOSE,
        ];
        // Close needs an 8-byte session_id; give it only 3 (short body).
        payload.extend_from_slice(&[1, 2, 3]);
        let checksum = session_record_checksum(&payload);
        let len = (payload.len() + SESSION_CHECKSUM_LEN) as u32;
        let mut frame = Vec::new();
        frame.extend_from_slice(&len.to_le_bytes());
        frame.extend_from_slice(&payload);
        frame.extend_from_slice(&checksum);

        let outcome = decode_session_events_with_diagnostics(&frame);
        assert!(outcome.events.is_empty(), "short body decodes no event");
        assert!(
            !outcome.corruption_detected,
            "a checksum-valid short body is a clean stop, not on-disk corruption"
        );
        assert_eq!(outcome.corruption_count, 0);
    }

    #[test]
    fn session_v2_short_body_after_valid_record_keeps_survivor() {
        // A valid record followed by a checksum-valid-but-short v2 frame keeps
        // the first record and stops cleanly without flagging corruption.
        let e1 = SessionWalEvent::Close { session_id: 5 };
        let mut bytes = enc(&e1);

        let mut payload = vec![
            SESSION_RECORD_VERSIONED,
            SESSION_RECORD_VERSION_V2,
            SESSION_RECORD_START,
        ];
        // Start needs a 24-byte fixed prefix; give it 4 (short body).
        payload.extend_from_slice(&[9, 9, 9, 9]);
        let checksum = session_record_checksum(&payload);
        let len = (payload.len() + SESSION_CHECKSUM_LEN) as u32;
        bytes.extend_from_slice(&len.to_le_bytes());
        bytes.extend_from_slice(&payload);
        bytes.extend_from_slice(&checksum);

        let outcome = decode_session_events_with_diagnostics(&bytes);
        assert_eq!(outcome.events, vec![e1]);
        assert!(!outcome.corruption_detected);
        assert_eq!(outcome.corruption_count, 0);
    }

    #[test]
    fn session_legacy_invalid_utf8_field_is_corruption() {
        // A bit-flip that produces invalid UTF-8 in a legacy frame's string
        // field must be detected (not silently rewritten to U+FFFD).
        let event = SessionWalEvent::Start {
            session_id: 1,
            user_id: 2,
            started_at_ns: 3,
            agent_id: "agent".to_string(),
            policy_name: "policy".to_string(),
        };
        let mut legacy = encode_session_event_v1_legacy(&event);
        // Locate the first byte of the agent_id string: after the 4-byte length
        // prefix, the 1-byte type, the 24-byte fixed Start prefix, and the
        // 2-byte agent_id length field. Overwrite it with a lone continuation
        // byte (0x80), which is invalid as the start of a UTF-8 sequence.
        let agent_start = SESSION_LEN_PREFIX_LEN + 1 + SESSION_START_FIXED_PREFIX + 2;
        legacy[agent_start] = 0x80;
        let outcome = decode_session_events_with_diagnostics(&legacy);
        assert!(outcome.events.is_empty());
        assert!(
            outcome.corruption_detected,
            "invalid UTF-8 must be flagged as corruption"
        );
        assert_eq!(outcome.corruption_count, 1);
    }

    // ── W30: over-length string fields are rejected, never silently truncated ──

    #[test]
    fn session_over_length_annotation_is_rejected() {
        // An annotation longer than u16::MAX bytes cannot fit the 16-bit length
        // prefix. The pre-fix bug cast `len() as u16`, wrapping the count while
        // appending the full bytes — a frame whose checksum passed but decoded
        // back truncated, misframing every later record. The encoder must now
        // reject it with a typed error rather than emit an unrecoverable record.
        let event = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 2,
            weight: 1.0,
            ts_ns: 3,
            signal_name: "reward".to_string(),
            annotation: Some("x".repeat(70_000)),
            session_seqno: None,
            idempotency_key: None,
        };
        let result = encode_session_event(&event);
        match result {
            Err(WalError::Corruption { message }) => {
                assert!(
                    message.contains("annotation") && message.contains("70000"),
                    "unexpected error message: {message}"
                );
            }
            other => panic!("expected Corruption for an over-length annotation, got {other:?}"),
        }
    }

    #[test]
    fn session_over_length_signal_name_is_rejected() {
        let event = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 2,
            weight: 1.0,
            ts_ns: 3,
            signal_name: "s".repeat(usize::from(u16::MAX) + 1),
            annotation: None,
            session_seqno: None,
            idempotency_key: None,
        };
        assert!(matches!(
            encode_session_event(&event),
            Err(WalError::Corruption { .. })
        ));
    }

    #[test]
    fn session_max_length_field_round_trips() {
        // Exactly u16::MAX bytes is the largest field the prefix can represent
        // and MUST round-trip losslessly — the rejection boundary is exclusive.
        let annotation = "y".repeat(usize::from(u16::MAX));
        let event = SessionWalEvent::Signal {
            session_id: 1,
            entity_id: 2,
            weight: 1.0,
            ts_ns: 3,
            signal_name: "view".to_string(),
            annotation: Some(annotation),
            session_seqno: None,
            idempotency_key: None,
        };
        let encoded = encode_session_event(&event).expect("u16::MAX-length field must encode");
        let decoded = decode_session_events(&encoded);
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0], event);
    }

    // ── wal-format SUG: present-but-short optional field stops, not silent None ──

    #[test]
    fn session_present_but_short_seqno_is_clean_stop_not_silent_none() {
        // Hand-build a checksum-valid v2 Signal frame whose session_seqno flag is
        // SET but whose 8-byte value is truncated. A genuine writer never emits
        // this (it always writes the full value when it sets the flag); only a
        // forged frame with a recomputed checksum can. The decoder must treat it
        // as a structural mismatch (clean stop, no event) rather than silently
        // decoding the present-but-short field to `None` and returning a Signal.
        let mut payload = vec![
            SESSION_RECORD_VERSIONED,
            SESSION_RECORD_VERSION_V2,
            SESSION_RECORD_SIGNAL,
        ];
        payload.extend_from_slice(&1u64.to_le_bytes()); // session_id
        payload.extend_from_slice(&2u64.to_le_bytes()); // entity_id
        payload.extend_from_slice(&1.0f64.to_le_bytes()); // weight
        payload.extend_from_slice(&3u64.to_le_bytes()); // ts_ns
        payload.extend_from_slice(&4u16.to_le_bytes()); // signal_name len = 4
        payload.extend_from_slice(b"view"); // signal_name
        payload.push(0u8); // has_annotation = false
        payload.push(1u8); // has_seqno = true ...
        payload.extend_from_slice(&[0xAA, 0xBB]); // ... but only 2 of 8 bytes
        let checksum = session_record_checksum(&payload);
        let len = (payload.len() + SESSION_CHECKSUM_LEN) as u32;
        let mut frame = Vec::new();
        frame.extend_from_slice(&len.to_le_bytes());
        frame.extend_from_slice(&payload);
        frame.extend_from_slice(&checksum);

        let outcome = decode_session_events_with_diagnostics(&frame);
        assert!(
            outcome.events.is_empty(),
            "a present-but-short optional field must NOT decode to a silent-None Signal"
        );
        // Checksum-valid bytes were proven intact, so this is a structural
        // (forward-compat/torn) mismatch, a clean stop — not on-disk corruption.
        assert!(!outcome.corruption_detected);
        assert_eq!(outcome.corruption_count, 0);
    }
}
