//! Session-layer message types: setup, ack, data, and error messages.

use super::ProtocolError;
use crate::NodeAddr;
use crate::tree::TreeCoordinate;
use std::fmt;

// ============================================================================
// Session Layer Message Types
// ============================================================================

/// SessionDatagram payload message type identifiers.
///
/// These messages are carried as payloads inside `SessionDatagram` (link
/// message type 0x00). Post-handshake messages (data, reports) are end-to-end
/// encrypted with session keys via the FSP pipeline. Error signals
/// (CoordsRequired, PathBroken) are plaintext messages generated by transit
/// routers that cannot establish e2e sessions with the source.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum SessionMessageType {
    // Session establishment (0x00-0x0F)
    /// Session setup with coordinates (warms router caches).
    SessionSetup = 0x00,
    /// Session acknowledgement.
    SessionAck = 0x01,

    // Data and metrics (0x10-0x1F) — encrypted, inner header msg_type
    /// Port-multiplexed service payload: `[src_port:2 LE][dst_port:2 LE][service data...]`.
    /// Port 256 = IPv6 shim (compressed header). Receiver dispatches by dst_port.
    DataPacket = 0x10,
    /// MMP sender report (metrics from sender to receiver).
    SenderReport = 0x11,
    /// MMP receiver report (metrics from receiver to sender).
    ReceiverReport = 0x12,
    /// Path MTU notification (discovered path MTU).
    PathMtuNotification = 0x13,
    /// Standalone coordinate cache warming (empty body, coords in CP flag).
    CoordsWarmup = 0x14,

    // Link-layer error signals (0x20-0x2F) — plaintext, from transit routers
    /// Router cache miss — needs coordinates (link-layer error signal).
    CoordsRequired = 0x20,
    /// Routing failure — local minimum or unreachable (link-layer error signal).
    PathBroken = 0x21,
    /// MTU exceeded — forwarded packet too large for next-hop transport (link-layer error signal).
    MtuExceeded = 0x22,
}

impl SessionMessageType {
    /// Try to convert from a byte.
    pub fn from_byte(b: u8) -> Option<Self> {
        match b {
            0x00 => Some(SessionMessageType::SessionSetup),
            0x01 => Some(SessionMessageType::SessionAck),
            0x10 => Some(SessionMessageType::DataPacket),
            0x11 => Some(SessionMessageType::SenderReport),
            0x12 => Some(SessionMessageType::ReceiverReport),
            0x13 => Some(SessionMessageType::PathMtuNotification),
            0x14 => Some(SessionMessageType::CoordsWarmup),
            0x20 => Some(SessionMessageType::CoordsRequired),
            0x21 => Some(SessionMessageType::PathBroken),
            0x22 => Some(SessionMessageType::MtuExceeded),
            _ => None,
        }
    }

    /// Convert to a byte.
    pub fn to_byte(self) -> u8 {
        self as u8
    }
}

impl fmt::Display for SessionMessageType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            SessionMessageType::SessionSetup => "SessionSetup",
            SessionMessageType::SessionAck => "SessionAck",
            SessionMessageType::DataPacket => "DataPacket",
            SessionMessageType::SenderReport => "SenderReport",
            SessionMessageType::ReceiverReport => "ReceiverReport",
            SessionMessageType::PathMtuNotification => "PathMtuNotification",
            SessionMessageType::CoordsWarmup => "CoordsWarmup",
            SessionMessageType::CoordsRequired => "CoordsRequired",
            SessionMessageType::PathBroken => "PathBroken",
            SessionMessageType::MtuExceeded => "MtuExceeded",
        };
        write!(f, "{}", name)
    }
}

// ============================================================================
// Coordinate Wire Format Helpers
// ============================================================================

/// Wire size of a TreeCoordinate in address-only format: 2 + entries × 16.
pub(crate) fn coords_wire_size(coords: &TreeCoordinate) -> usize {
    2 + coords.entries().len() * 16
}

/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n).
///
/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each),
/// without the sequence/timestamp metadata used by the tree gossip protocol.
pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec<u8>) {
    let addrs: Vec<&NodeAddr> = coords.node_addrs().collect();
    let count = addrs.len() as u16;
    buf.extend_from_slice(&count.to_le_bytes());
    for addr in addrs {
        buf.extend_from_slice(addr.as_bytes());
    }
}

/// Decode a TreeCoordinate from address-only wire format.
///
/// Returns the decoded coordinate and the number of bytes consumed.
pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), ProtocolError> {
    if data.len() < 2 {
        return Err(ProtocolError::MessageTooShort {
            expected: 2,
            got: data.len(),
        });
    }
    let count = u16::from_le_bytes([data[0], data[1]]) as usize;
    let needed = 2 + count * 16;
    if data.len() < needed {
        return Err(ProtocolError::MessageTooShort {
            expected: needed,
            got: data.len(),
        });
    }
    if count == 0 {
        return Err(ProtocolError::Malformed(
            "coordinate with zero entries".into(),
        ));
    }
    let mut addrs = Vec::with_capacity(count);
    for i in 0..count {
        let offset = 2 + i * 16;
        let mut bytes = [0u8; 16];
        bytes.copy_from_slice(&data[offset..offset + 16]);
        addrs.push(NodeAddr::from_bytes(bytes));
    }
    let coord =
        TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
    Ok((coord, needed))
}

/// Decode an optional coordinate field (count may be 0).
///
/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed.
pub(crate) fn decode_optional_coords(
    data: &[u8],
) -> Result<(Option<TreeCoordinate>, usize), ProtocolError> {
    if data.len() < 2 {
        return Err(ProtocolError::MessageTooShort {
            expected: 2,
            got: data.len(),
        });
    }
    let count = u16::from_le_bytes([data[0], data[1]]) as usize;
    let needed = 2 + count * 16;
    if data.len() < needed {
        return Err(ProtocolError::MessageTooShort {
            expected: needed,
            got: data.len(),
        });
    }
    if count == 0 {
        return Ok((None, 2));
    }
    let mut addrs = Vec::with_capacity(count);
    for i in 0..count {
        let offset = 2 + i * 16;
        let mut bytes = [0u8; 16];
        bytes.copy_from_slice(&data[offset..offset + 16]);
        addrs.push(NodeAddr::from_bytes(bytes));
    }
    let coord =
        TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
    Ok((Some(coord), needed))
}

/// Encode a count of zero (for empty/absent coordinate fields).
fn encode_empty_coords(buf: &mut Vec<u8>) {
    buf.extend_from_slice(&0u16.to_le_bytes());
}

// ============================================================================
// Session Flags
// ============================================================================

/// Session flags for setup options.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SessionFlags {
    /// Request acknowledgement from destination.
    pub request_ack: bool,
    /// Set up bidirectional session.
    pub bidirectional: bool,
}

impl SessionFlags {
    /// Create default flags.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set request_ack flag.
    pub fn with_ack(mut self) -> Self {
        self.request_ack = true;
        self
    }

    /// Set bidirectional flag.
    pub fn bidirectional(mut self) -> Self {
        self.bidirectional = true;
        self
    }

    /// Convert to a byte.
    pub fn to_byte(&self) -> u8 {
        let mut flags = 0u8;
        if self.request_ack {
            flags |= 0x01;
        }
        if self.bidirectional {
            flags |= 0x02;
        }
        flags
    }

    /// Convert from a byte.
    pub fn from_byte(byte: u8) -> Self {
        Self {
            request_ack: byte & 0x01 != 0,
            bidirectional: byte & 0x02 != 0,
        }
    }
}

// ============================================================================
// FSP Packet Flags
// ============================================================================

/// FSP common prefix flags (cleartext, in outer header).
///
/// | Bit | Name | Description                                    |
/// |-----|------|------------------------------------------------|
/// | 0   | CP   | Coords present between header and ciphertext   |
/// | 1   | K    | Key epoch (for rekeying)                       |
/// | 2   | U    | Unencrypted payload (error signals)            |
/// | 3-7 |      | Reserved                                       |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FspFlags {
    /// Coordinates present between header and ciphertext.
    pub coords_present: bool,
    /// Key epoch bit for rekeying.
    pub key_epoch: bool,
    /// Unencrypted payload (plaintext error signals from transit routers).
    pub unencrypted: bool,
}

impl FspFlags {
    /// Create default flags (all clear).
    pub fn new() -> Self {
        Self::default()
    }

    /// Convert to a byte.
    pub fn to_byte(&self) -> u8 {
        let mut flags = 0u8;
        if self.coords_present {
            flags |= 0x01;
        }
        if self.key_epoch {
            flags |= 0x02;
        }
        if self.unencrypted {
            flags |= 0x04;
        }
        flags
    }

    /// Convert from a byte.
    pub fn from_byte(byte: u8) -> Self {
        Self {
            coords_present: byte & 0x01 != 0,
            key_epoch: byte & 0x02 != 0,
            unencrypted: byte & 0x04 != 0,
        }
    }
}

/// FSP inner header flags (encrypted, inside AEAD envelope).
///
/// | Bit | Name | Description                     |
/// |-----|------|---------------------------------|
/// | 0   | SP   | Spin bit for RTT measurement    |
/// | 1-7 |      | Reserved                        |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FspInnerFlags {
    /// Spin bit for passive RTT measurement.
    pub spin_bit: bool,
}

impl FspInnerFlags {
    /// Create default inner flags (all clear).
    pub fn new() -> Self {
        Self::default()
    }

    /// Convert to a byte.
    pub fn to_byte(&self) -> u8 {
        if self.spin_bit { 0x01 } else { 0x00 }
    }

    /// Convert from a byte.
    pub fn from_byte(byte: u8) -> Self {
        Self {
            spin_bit: byte & 0x01 != 0,
        }
    }
}

// ============================================================================
// Session Setup
// ============================================================================

/// Session setup to establish cached coordinate state.
///
/// Carried inside a SessionDatagram envelope which provides src_addr and
/// dest_addr. The SessionSetup payload contains coordinates, session flags,
/// and the Noise IK handshake message for session establishment.
///
/// ## Wire Format
///
/// | Offset | Field            | Size    | Description                         |
/// |--------|------------------|---------|-------------------------------------|
/// | 0      | msg_type         | 1 byte  | 0x00                                |
/// | 1      | flags            | 1 byte  | Bit 0: REQUEST_ACK, Bit 1: BIDIR   |
/// | 2      | src_coords_count | 2 bytes | u16 LE, number of src coord entries |
/// | 4      | src_coords       | 16 × n  | NodeAddr array (self → root)        |
/// | ...    | dest_coords_count| 2 bytes | u16 LE, number of dest coord entries|
/// | ...    | dest_coords      | 16 × m  | NodeAddr array (dest → root)        |
/// | ...    | handshake_len    | 2 bytes  | u16 LE, Noise payload length        |
/// | ...    | handshake_payload| variable| Noise IK msg1 (82 bytes typical)    |
#[derive(Clone, Debug)]
pub struct SessionSetup {
    /// Source coordinates (for return path caching).
    pub src_coords: TreeCoordinate,
    /// Destination coordinates (for forward routing).
    pub dest_coords: TreeCoordinate,
    /// Session options.
    pub flags: SessionFlags,
    /// Noise IK handshake message 1.
    pub handshake_payload: Vec<u8>,
}

impl SessionSetup {
    /// Create a new session setup message.
    pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
        Self {
            src_coords,
            dest_coords,
            flags: SessionFlags::new(),
            handshake_payload: Vec::new(),
        }
    }

    /// Set session flags.
    pub fn with_flags(mut self, flags: SessionFlags) -> Self {
        self.flags = flags;
        self
    }

    /// Set the Noise handshake payload.
    pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
        self.handshake_payload = payload;
        self
    }

    /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
    ///
    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
    /// where ver_phase = 0x01 (version 0, phase MSG1).
    pub fn encode(&self) -> Vec<u8> {
        // Build body first to compute payload_len
        let mut body = Vec::new();
        body.push(self.flags.to_byte());
        encode_coords(&self.src_coords, &mut body);
        encode_coords(&self.dest_coords, &mut body);
        let hs_len = self.handshake_payload.len() as u16;
        body.extend_from_slice(&hs_len.to_le_bytes());
        body.extend_from_slice(&self.handshake_payload);

        // Prepend 4-byte FSP common prefix
        let payload_len = body.len() as u16;
        let mut buf = Vec::with_capacity(4 + body.len());
        buf.push(0x01); // version 0, phase 0x1 (MSG1)
        buf.push(0x00); // flags (must be zero for handshake)
        buf.extend_from_slice(&payload_len.to_le_bytes());
        buf.extend_from_slice(&body);
        buf
    }

    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        if payload.is_empty() {
            return Err(ProtocolError::MessageTooShort {
                expected: 1,
                got: 0,
            });
        }
        let flags = SessionFlags::from_byte(payload[0]);
        let mut offset = 1;

        let (src_coords, consumed) = decode_coords(&payload[offset..])?;
        offset += consumed;

        let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
        offset += consumed;

        if payload.len() < offset + 2 {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + 2,
                got: payload.len(),
            });
        }
        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
        offset += 2;

        if payload.len() < offset + hs_len {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + hs_len,
                got: payload.len(),
            });
        }
        let handshake_payload = payload[offset..offset + hs_len].to_vec();

        Ok(Self {
            src_coords,
            dest_coords,
            flags,
            handshake_payload,
        })
    }
}

// ============================================================================
// Session Ack
// ============================================================================

/// Session acknowledgement.
///
/// Carried inside a SessionDatagram envelope which provides src_addr and
/// dest_addr. The SessionAck payload contains both the acknowledger's and
/// initiator's coordinates for route cache warming (ensuring return-path
/// transit nodes can route independently of the forward path) and the Noise
/// IK handshake response.
///
/// ## Wire Format
///
/// | Offset | Field            | Size    | Description                         |
/// |--------|------------------|---------|-------------------------------------|
/// | 0      | msg_type         | 1 byte  | 0x01                                |
/// | 1      | flags            | 1 byte  | Reserved                            |
/// | 2      | src_coords_count | 2 bytes | u16 LE                              |
/// | 4      | src_coords       | 16 × n  | Acknowledger's coords (for caching) |
/// | ...    | dest_coords_count| 2 bytes | u16 LE                              |
/// | ...    | dest_coords      | 16 × m  | Initiator's coords (for return path)|
/// | ...    | handshake_len    | 2 bytes  | u16 LE, Noise payload length        |
/// | ...    | handshake_payload| variable| Noise IK msg2 (33 bytes typical)    |
#[derive(Clone, Debug)]
pub struct SessionAck {
    /// Acknowledger's coordinates.
    pub src_coords: TreeCoordinate,
    /// Initiator's coordinates (for return-path cache warming).
    pub dest_coords: TreeCoordinate,
    /// Reserved flags byte (for forward compatibility).
    pub flags: u8,
    /// Noise IK handshake message 2.
    pub handshake_payload: Vec<u8>,
}

impl SessionAck {
    /// Create a new session acknowledgement.
    pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
        Self {
            src_coords,
            dest_coords,
            flags: 0,
            handshake_payload: Vec::new(),
        }
    }

    /// Set the Noise handshake payload.
    pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
        self.handshake_payload = payload;
        self
    }

    /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
    ///
    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
    /// where ver_phase = 0x02 (version 0, phase MSG2).
    pub fn encode(&self) -> Vec<u8> {
        // Build body first to compute payload_len
        let mut body = Vec::new();
        body.push(self.flags);
        encode_coords(&self.src_coords, &mut body);
        encode_coords(&self.dest_coords, &mut body);
        let hs_len = self.handshake_payload.len() as u16;
        body.extend_from_slice(&hs_len.to_le_bytes());
        body.extend_from_slice(&self.handshake_payload);

        // Prepend 4-byte FSP common prefix
        let payload_len = body.len() as u16;
        let mut buf = Vec::with_capacity(4 + body.len());
        buf.push(0x02); // version 0, phase 0x2 (MSG2)
        buf.push(0x00); // flags (must be zero for handshake)
        buf.extend_from_slice(&payload_len.to_le_bytes());
        buf.extend_from_slice(&body);
        buf
    }

    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        if payload.is_empty() {
            return Err(ProtocolError::MessageTooShort {
                expected: 1,
                got: 0,
            });
        }
        let flags = payload[0];
        let mut offset = 1;

        let (src_coords, consumed) = decode_coords(&payload[offset..])?;
        offset += consumed;

        let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
        offset += consumed;

        if payload.len() < offset + 2 {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + 2,
                got: payload.len(),
            });
        }
        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
        offset += 2;

        if payload.len() < offset + hs_len {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + hs_len,
                got: payload.len(),
            });
        }
        let handshake_payload = payload[offset..offset + hs_len].to_vec();

        Ok(Self {
            src_coords,
            dest_coords,
            flags,
            handshake_payload,
        })
    }
}

// ============================================================================
// Session Msg3 (XK Handshake Message 3)
// ============================================================================

/// XK handshake message 3 (initiator -> responder).
///
/// Carries the initiator's encrypted static key and epoch. Sent by the
/// initiator after receiving msg2. The responder learns the initiator's
/// identity from this message.
///
/// ## Wire Format
///
/// | Offset | Field            | Size    | Description                         |
/// |--------|------------------|---------|-------------------------------------|
/// | 0      | flags            | 1 byte  | Reserved                            |
/// | 1      | handshake_len    | 2 bytes | u16 LE, Noise payload length        |
/// | 3      | handshake_payload| variable| Noise XK msg3 (73 bytes typical)    |
#[derive(Clone, Debug)]
pub struct SessionMsg3 {
    /// Reserved flags byte.
    pub flags: u8,
    /// Noise XK handshake message 3.
    pub handshake_payload: Vec<u8>,
}

impl SessionMsg3 {
    /// Create a new SessionMsg3 with the given handshake payload.
    pub fn new(handshake_payload: Vec<u8>) -> Self {
        Self {
            flags: 0,
            handshake_payload,
        }
    }

    /// Encode as wire format (4-byte FSP prefix + flags + handshake).
    ///
    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
    /// where ver_phase = 0x03 (version 0, phase MSG3).
    pub fn encode(&self) -> Vec<u8> {
        // Build body first to compute payload_len
        let mut body = Vec::new();
        body.push(self.flags);
        let hs_len = self.handshake_payload.len() as u16;
        body.extend_from_slice(&hs_len.to_le_bytes());
        body.extend_from_slice(&self.handshake_payload);

        // Prepend 4-byte FSP common prefix
        let payload_len = body.len() as u16;
        let mut buf = Vec::with_capacity(4 + body.len());
        buf.push(0x03); // version 0, phase 0x3 (MSG3)
        buf.push(0x00); // flags (must be zero for handshake)
        buf.extend_from_slice(&payload_len.to_le_bytes());
        buf.extend_from_slice(&body);
        buf
    }

    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        if payload.is_empty() {
            return Err(ProtocolError::MessageTooShort {
                expected: 1,
                got: 0,
            });
        }
        let flags = payload[0];
        let mut offset = 1;

        if payload.len() < offset + 2 {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + 2,
                got: payload.len(),
            });
        }
        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
        offset += 2;

        if payload.len() < offset + hs_len {
            return Err(ProtocolError::MessageTooShort {
                expected: offset + hs_len,
                got: payload.len(),
            });
        }
        let handshake_payload = payload[offset..offset + hs_len].to_vec();

        Ok(Self {
            flags,
            handshake_payload,
        })
    }
}

// ============================================================================
// Session-Layer MMP Reports
// ============================================================================

/// Session-layer sender report (msg_type 0x11).
///
/// Mirrors the FMP `SenderReport` fields but carried as an FSP session
/// message inside the AEAD envelope. The msg_type is in the FSP inner
/// header, so the body starts with reserved bytes.
///
/// ## Wire Format (46 bytes body, after inner header stripped)
///
/// ```text
/// [0-1]   reserved (zero)
/// [2-9]   interval_start_counter: u64 LE
/// [10-17] interval_end_counter: u64 LE
/// [18-21] interval_start_timestamp: u32 LE
/// [22-25] interval_end_timestamp: u32 LE
/// [26-29] interval_bytes_sent: u32 LE
/// [30-37] cumulative_packets_sent: u64 LE
/// [38-45] cumulative_bytes_sent: u64 LE
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSenderReport {
    pub interval_start_counter: u64,
    pub interval_end_counter: u64,
    pub interval_start_timestamp: u32,
    pub interval_end_timestamp: u32,
    pub interval_bytes_sent: u32,
    pub cumulative_packets_sent: u64,
    pub cumulative_bytes_sent: u64,
}

/// Body size for SessionSenderReport: 2 reserved + 44 fields.
pub const SESSION_SENDER_REPORT_SIZE: usize = 46;

impl SessionSenderReport {
    /// Encode to wire format (46 bytes body).
    pub fn encode(&self) -> Vec<u8> {
        let mut buf = Vec::with_capacity(SESSION_SENDER_REPORT_SIZE);
        buf.extend_from_slice(&[0u8; 2]); // reserved
        buf.extend_from_slice(&self.interval_start_counter.to_le_bytes());
        buf.extend_from_slice(&self.interval_end_counter.to_le_bytes());
        buf.extend_from_slice(&self.interval_start_timestamp.to_le_bytes());
        buf.extend_from_slice(&self.interval_end_timestamp.to_le_bytes());
        buf.extend_from_slice(&self.interval_bytes_sent.to_le_bytes());
        buf.extend_from_slice(&self.cumulative_packets_sent.to_le_bytes());
        buf.extend_from_slice(&self.cumulative_bytes_sent.to_le_bytes());
        buf
    }

    /// Decode from body (after FSP inner header has been stripped).
    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
        if body.len() < SESSION_SENDER_REPORT_SIZE {
            return Err(ProtocolError::MessageTooShort {
                expected: SESSION_SENDER_REPORT_SIZE,
                got: body.len(),
            });
        }
        // Skip 2 reserved bytes
        let p = &body[2..];
        Ok(Self {
            interval_start_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
            interval_end_counter: u64::from_le_bytes(p[8..16].try_into().unwrap()),
            interval_start_timestamp: u32::from_le_bytes(p[16..20].try_into().unwrap()),
            interval_end_timestamp: u32::from_le_bytes(p[20..24].try_into().unwrap()),
            interval_bytes_sent: u32::from_le_bytes(p[24..28].try_into().unwrap()),
            cumulative_packets_sent: u64::from_le_bytes(p[28..36].try_into().unwrap()),
            cumulative_bytes_sent: u64::from_le_bytes(p[36..44].try_into().unwrap()),
        })
    }
}

/// Session-layer receiver report (msg_type 0x12).
///
/// Mirrors the FMP `ReceiverReport` fields but carried as an FSP session
/// message inside the AEAD envelope.
///
/// ## Wire Format (66 bytes body, after inner header stripped)
///
/// ```text
/// [0-1]   reserved (zero)
/// [2-9]   highest_counter: u64 LE
/// [10-17] cumulative_packets_recv: u64 LE
/// [18-25] cumulative_bytes_recv: u64 LE
/// [26-29] timestamp_echo: u32 LE
/// [30-31] dwell_time: u16 LE
/// [32-33] max_burst_loss: u16 LE
/// [34-35] mean_burst_loss: u16 LE (u8.8 fixed-point)
/// [36-37] reserved: u16 LE
/// [38-41] jitter: u32 LE (microseconds)
/// [42-45] ecn_ce_count: u32 LE
/// [46-49] owd_trend: i32 LE (µs/s)
/// [50-53] burst_loss_count: u32 LE
/// [54-57] cumulative_reorder_count: u32 LE
/// [58-61] interval_packets_recv: u32 LE
/// [62-65] interval_bytes_recv: u32 LE
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionReceiverReport {
    pub highest_counter: u64,
    pub cumulative_packets_recv: u64,
    pub cumulative_bytes_recv: u64,
    pub timestamp_echo: u32,
    pub dwell_time: u16,
    pub max_burst_loss: u16,
    pub mean_burst_loss: u16,
    pub jitter: u32,
    pub ecn_ce_count: u32,
    pub owd_trend: i32,
    pub burst_loss_count: u32,
    pub cumulative_reorder_count: u32,
    pub interval_packets_recv: u32,
    pub interval_bytes_recv: u32,
}

/// Body size for SessionReceiverReport: 2 reserved + 64 fields.
pub const SESSION_RECEIVER_REPORT_SIZE: usize = 66;

impl SessionReceiverReport {
    /// Encode to wire format (66 bytes body).
    pub fn encode(&self) -> Vec<u8> {
        let mut buf = Vec::with_capacity(SESSION_RECEIVER_REPORT_SIZE);
        buf.extend_from_slice(&[0u8; 2]); // reserved
        buf.extend_from_slice(&self.highest_counter.to_le_bytes());
        buf.extend_from_slice(&self.cumulative_packets_recv.to_le_bytes());
        buf.extend_from_slice(&self.cumulative_bytes_recv.to_le_bytes());
        buf.extend_from_slice(&self.timestamp_echo.to_le_bytes());
        buf.extend_from_slice(&self.dwell_time.to_le_bytes());
        buf.extend_from_slice(&self.max_burst_loss.to_le_bytes());
        buf.extend_from_slice(&self.mean_burst_loss.to_le_bytes());
        buf.extend_from_slice(&[0u8; 2]); // reserved
        buf.extend_from_slice(&self.jitter.to_le_bytes());
        buf.extend_from_slice(&self.ecn_ce_count.to_le_bytes());
        buf.extend_from_slice(&self.owd_trend.to_le_bytes());
        buf.extend_from_slice(&self.burst_loss_count.to_le_bytes());
        buf.extend_from_slice(&self.cumulative_reorder_count.to_le_bytes());
        buf.extend_from_slice(&self.interval_packets_recv.to_le_bytes());
        buf.extend_from_slice(&self.interval_bytes_recv.to_le_bytes());
        buf
    }

    /// Decode from body (after FSP inner header has been stripped).
    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
        if body.len() < SESSION_RECEIVER_REPORT_SIZE {
            return Err(ProtocolError::MessageTooShort {
                expected: SESSION_RECEIVER_REPORT_SIZE,
                got: body.len(),
            });
        }
        // Skip 2 reserved bytes
        let p = &body[2..];
        Ok(Self {
            highest_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
            cumulative_packets_recv: u64::from_le_bytes(p[8..16].try_into().unwrap()),
            cumulative_bytes_recv: u64::from_le_bytes(p[16..24].try_into().unwrap()),
            timestamp_echo: u32::from_le_bytes(p[24..28].try_into().unwrap()),
            dwell_time: u16::from_le_bytes(p[28..30].try_into().unwrap()),
            max_burst_loss: u16::from_le_bytes(p[30..32].try_into().unwrap()),
            mean_burst_loss: u16::from_le_bytes(p[32..34].try_into().unwrap()),
            // skip 2 reserved bytes at p[34..36]
            jitter: u32::from_le_bytes(p[36..40].try_into().unwrap()),
            ecn_ce_count: u32::from_le_bytes(p[40..44].try_into().unwrap()),
            owd_trend: i32::from_le_bytes(p[44..48].try_into().unwrap()),
            burst_loss_count: u32::from_le_bytes(p[48..52].try_into().unwrap()),
            cumulative_reorder_count: u32::from_le_bytes(p[52..56].try_into().unwrap()),
            interval_packets_recv: u32::from_le_bytes(p[56..60].try_into().unwrap()),
            interval_bytes_recv: u32::from_le_bytes(p[60..64].try_into().unwrap()),
        })
    }
}

/// Path MTU notification (msg_type 0x13).
///
/// Sent by a node that discovers a path MTU value (from transit router
/// feedback or ICMP Packet Too Big). Allows the remote endpoint to
/// adjust its sending MTU.
///
/// ## Wire Format (2 bytes body, after inner header stripped)
///
/// ```text
/// [0-1]   path_mtu: u16 LE
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathMtuNotification {
    /// Discovered path MTU in bytes.
    pub path_mtu: u16,
}

/// Body size for PathMtuNotification.
pub const PATH_MTU_NOTIFICATION_SIZE: usize = 2;

impl PathMtuNotification {
    /// Create a new path MTU notification.
    pub fn new(path_mtu: u16) -> Self {
        Self { path_mtu }
    }

    /// Encode to wire format (2 bytes body).
    pub fn encode(&self) -> Vec<u8> {
        self.path_mtu.to_le_bytes().to_vec()
    }

    /// Decode from body (after FSP inner header has been stripped).
    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
        if body.len() < PATH_MTU_NOTIFICATION_SIZE {
            return Err(ProtocolError::MessageTooShort {
                expected: PATH_MTU_NOTIFICATION_SIZE,
                got: body.len(),
            });
        }
        Ok(Self {
            path_mtu: u16::from_le_bytes([body[0], body[1]]),
        })
    }
}

// ============================================================================
// Error Messages
// ============================================================================

/// Link-layer error signal indicating router cache miss.
///
/// Generated by a transit router when it cannot forward a SessionDatagram
/// due to missing cached coordinates for the destination. Carried inside
/// a new SessionDatagram addressed back to the original source
/// (src_addr=reporter, dest_addr=original_source). Plaintext — not
/// end-to-end encrypted, since the transit router has no session with
/// the source.
///
/// ## Wire Format
///
/// | Offset | Field    | Size     | Description                        |
/// |--------|----------|---------|------------------------------------|
/// | 0      | msg_type | 1 byte  | 0x20                               |
/// | 1      | flags    | 1 byte  | Reserved                           |
/// | 2      | dest_addr| 16 bytes| The node_addr we couldn't route to |
/// | 18     | reporter | 16 bytes| NodeAddr of reporting router       |
///
/// Payload: 34 bytes
#[derive(Clone, Debug)]
pub struct CoordsRequired {
    /// Destination that couldn't be routed.
    pub dest_addr: NodeAddr,
    /// Router reporting the miss.
    pub reporter: NodeAddr,
}

/// Wire size of CoordsRequired payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16).
pub const COORDS_REQUIRED_SIZE: usize = 34;

impl CoordsRequired {
    /// Create a new CoordsRequired error.
    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
        Self {
            dest_addr,
            reporter,
        }
    }

    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
    ///
    /// Error signals use phase=0x0 with U flag set.
    pub fn encode(&self) -> Vec<u8> {
        // Body: msg_type + flags(reserved) + dest_addr + reporter
        let body_len = 1 + 1 + 16 + 16; // 34 bytes
        let mut buf = Vec::with_capacity(4 + body_len);
        // FSP prefix: version 0, phase 0x0, U flag set
        buf.push(0x00); // version 0, phase 0x0
        buf.push(0x04); // U flag
        let payload_len = body_len as u16;
        buf.extend_from_slice(&payload_len.to_le_bytes());
        // msg_type byte (after prefix, before body)
        buf.push(SessionMessageType::CoordsRequired.to_byte());
        buf.push(0x00); // reserved flags
        buf.extend_from_slice(self.dest_addr.as_bytes());
        buf.extend_from_slice(self.reporter.as_bytes());
        buf
    }

    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        // flags(1) + dest_addr(16) + reporter(16) = 33
        if payload.len() < 33 {
            return Err(ProtocolError::MessageTooShort {
                expected: 33,
                got: payload.len(),
            });
        }
        // payload[0] is flags (reserved, ignored)
        let mut dest_bytes = [0u8; 16];
        dest_bytes.copy_from_slice(&payload[1..17]);
        let mut reporter_bytes = [0u8; 16];
        reporter_bytes.copy_from_slice(&payload[17..33]);

        Ok(Self {
            dest_addr: NodeAddr::from_bytes(dest_bytes),
            reporter: NodeAddr::from_bytes(reporter_bytes),
        })
    }
}

/// Error indicating routing failure (local minimum or unreachable).
///
/// Carried inside a SessionDatagram addressed back to the original source.
/// The reporting router creates a new SessionDatagram with src_addr=reporter
/// and dest_addr=original_source, so the `original_src` field from the old
/// design is no longer needed — it's the SessionDatagram's dest_addr.
///
/// ## Wire Format
///
/// | Offset | Field             | Size     | Description                   |
/// |--------|-------------------|----------|-------------------------------|
/// | 0      | msg_type          | 1 byte   | 0x21                          |
/// | 1      | flags             | 1 byte   | Reserved                      |
/// | 2      | dest_addr         | 16 bytes | The unreachable node_addr     |
/// | 18     | reporter          | 16 bytes | NodeAddr of reporting router   |
/// | 34     | last_coords_count | 2 bytes  | u16 LE                        |
/// | 36     | last_known_coords | 16 × n   | Stale coords that failed      |
#[derive(Clone, Debug)]
pub struct PathBroken {
    /// Destination that couldn't be reached.
    pub dest_addr: NodeAddr,
    /// Node that detected the failure.
    pub reporter: NodeAddr,
    /// Optional: last known coordinates of destination.
    pub last_known_coords: Option<TreeCoordinate>,
}

impl PathBroken {
    /// Create a new PathBroken error.
    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
        Self {
            dest_addr,
            reporter,
            last_known_coords: None,
        }
    }

    /// Add last known coordinates.
    pub fn with_last_coords(mut self, coords: TreeCoordinate) -> Self {
        self.last_known_coords = Some(coords);
        self
    }

    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
    ///
    /// Error signals use phase=0x0 with U flag set.
    pub fn encode(&self) -> Vec<u8> {
        // Build body first to compute length
        let mut body = Vec::new();
        body.push(SessionMessageType::PathBroken.to_byte());
        body.push(0x00); // reserved flags
        body.extend_from_slice(self.dest_addr.as_bytes());
        body.extend_from_slice(self.reporter.as_bytes());
        if let Some(ref coords) = self.last_known_coords {
            encode_coords(coords, &mut body);
        } else {
            encode_empty_coords(&mut body);
        }

        // Prepend FSP prefix: version 0, phase 0x0, U flag set
        let payload_len = body.len() as u16;
        let mut buf = Vec::with_capacity(4 + body.len());
        buf.push(0x00); // version 0, phase 0x0
        buf.push(0x04); // U flag
        buf.extend_from_slice(&payload_len.to_le_bytes());
        buf.extend_from_slice(&body);
        buf
    }

    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        // flags(1) + dest_addr(16) + reporter(16) + coords_count(2) = 35 minimum
        if payload.len() < 35 {
            return Err(ProtocolError::MessageTooShort {
                expected: 35,
                got: payload.len(),
            });
        }
        // payload[0] is flags (reserved, ignored)
        let mut dest_bytes = [0u8; 16];
        dest_bytes.copy_from_slice(&payload[1..17]);
        let mut reporter_bytes = [0u8; 16];
        reporter_bytes.copy_from_slice(&payload[17..33]);

        let (last_known_coords, _consumed) = decode_optional_coords(&payload[33..])?;

        Ok(Self {
            dest_addr: NodeAddr::from_bytes(dest_bytes),
            reporter: NodeAddr::from_bytes(reporter_bytes),
            last_known_coords,
        })
    }
}

/// Error indicating a forwarded packet exceeded the next-hop transport MTU.
///
/// Generated by a transit router when `send_encrypted_link_message()`
/// fails with `TransportError::MtuExceeded`. The reporter includes the
/// bottleneck MTU so the source can immediately reduce its sending MTU.
///
/// ## Wire Format
///
/// | Offset | Field     | Size     | Description                        |
/// |--------|-----------|----------|------------------------------------|
/// | 0      | msg_type  | 1 byte   | 0x22                               |
/// | 1      | flags     | 1 byte   | Reserved                           |
/// | 2      | dest_addr | 16 bytes | The destination we were forwarding |
/// | 18     | reporter  | 16 bytes | NodeAddr of reporting router       |
/// | 34     | mtu       | 2 bytes  | Bottleneck MTU (u16 LE)            |
///
/// Payload: 36 bytes
#[derive(Clone, Debug)]
pub struct MtuExceeded {
    /// Destination that the oversized packet was heading to.
    pub dest_addr: NodeAddr,
    /// Router that detected the MTU violation.
    pub reporter: NodeAddr,
    /// Transport MTU at the bottleneck hop.
    pub mtu: u16,
}

/// Wire size of MtuExceeded payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16) + mtu(2).
pub const MTU_EXCEEDED_SIZE: usize = 36;

impl MtuExceeded {
    /// Create a new MtuExceeded error.
    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr, mtu: u16) -> Self {
        Self {
            dest_addr,
            reporter,
            mtu,
        }
    }

    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
    ///
    /// Error signals use phase=0x0 with U flag set.
    pub fn encode(&self) -> Vec<u8> {
        let body_len = MTU_EXCEEDED_SIZE; // 36 bytes
        let mut buf = Vec::with_capacity(4 + body_len);
        // FSP prefix: version 0, phase 0x0, U flag set
        buf.push(0x00); // version 0, phase 0x0
        buf.push(0x04); // U flag
        let payload_len = body_len as u16;
        buf.extend_from_slice(&payload_len.to_le_bytes());
        // msg_type byte
        buf.push(SessionMessageType::MtuExceeded.to_byte());
        buf.push(0x00); // reserved flags
        buf.extend_from_slice(self.dest_addr.as_bytes());
        buf.extend_from_slice(self.reporter.as_bytes());
        buf.extend_from_slice(&self.mtu.to_le_bytes());
        buf
    }

    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        // flags(1) + dest_addr(16) + reporter(16) + mtu(2) = 35
        if payload.len() < 35 {
            return Err(ProtocolError::MessageTooShort {
                expected: 35,
                got: payload.len(),
            });
        }
        // payload[0] is flags (reserved, ignored)
        let mut dest_bytes = [0u8; 16];
        dest_bytes.copy_from_slice(&payload[1..17]);
        let mut reporter_bytes = [0u8; 16];
        reporter_bytes.copy_from_slice(&payload[17..33]);
        let mtu = u16::from_le_bytes([payload[33], payload[34]]);

        Ok(Self {
            dest_addr: NodeAddr::from_bytes(dest_bytes),
            reporter: NodeAddr::from_bytes(reporter_bytes),
            mtu,
        })
    }
}

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

    fn make_node_addr(val: u8) -> NodeAddr {
        let mut bytes = [0u8; 16];
        bytes[0] = val;
        NodeAddr::from_bytes(bytes)
    }

    fn make_coords(ids: &[u8]) -> TreeCoordinate {
        TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
    }

    // ===== SessionMessageType Tests =====

    #[test]
    fn test_session_message_type_roundtrip() {
        let types = [
            SessionMessageType::SessionSetup,
            SessionMessageType::SessionAck,
            SessionMessageType::DataPacket,
            SessionMessageType::SenderReport,
            SessionMessageType::ReceiverReport,
            SessionMessageType::PathMtuNotification,
            SessionMessageType::CoordsRequired,
            SessionMessageType::PathBroken,
            SessionMessageType::MtuExceeded,
        ];

        for ty in types {
            let byte = ty.to_byte();
            let restored = SessionMessageType::from_byte(byte);
            assert_eq!(restored, Some(ty));
        }
    }

    #[test]
    fn test_session_message_type_invalid() {
        assert!(SessionMessageType::from_byte(0xFF).is_none());
        assert!(SessionMessageType::from_byte(0x99).is_none());
    }

    // ===== SessionFlags Tests =====

    #[test]
    fn test_session_flags() {
        let flags = SessionFlags::new().with_ack().bidirectional();

        assert!(flags.request_ack);
        assert!(flags.bidirectional);

        let byte = flags.to_byte();
        let restored = SessionFlags::from_byte(byte);

        assert_eq!(flags, restored);
    }

    #[test]
    fn test_session_flags_default() {
        let flags = SessionFlags::new();
        assert!(!flags.request_ack);
        assert!(!flags.bidirectional);
        assert_eq!(flags.to_byte(), 0);
    }

    // ===== SessionSetup Tests =====

    #[test]
    fn test_session_setup() {
        let setup = SessionSetup::new(make_coords(&[1, 0]), make_coords(&[2, 0]))
            .with_flags(SessionFlags::new().with_ack());

        assert!(setup.flags.request_ack);
        assert!(!setup.flags.bidirectional);
    }

    // ===== CoordsRequired Tests =====

    #[test]
    fn test_coords_required() {
        let err = CoordsRequired::new(make_node_addr(1), make_node_addr(2));

        assert_eq!(err.dest_addr, make_node_addr(1));
        assert_eq!(err.reporter, make_node_addr(2));
    }

    // ===== PathBroken Tests =====

    #[test]
    fn test_path_broken() {
        let err = PathBroken::new(make_node_addr(2), make_node_addr(3))
            .with_last_coords(make_coords(&[2, 0]));

        assert_eq!(err.dest_addr, make_node_addr(2));
        assert_eq!(err.reporter, make_node_addr(3));
        assert!(err.last_known_coords.is_some());
    }

    // ===== Encode/Decode Roundtrip Tests =====

    #[test]
    fn test_session_setup_encode_decode() {
        let handshake = vec![0xAA; 82]; // typical Noise IK msg1
        let setup = SessionSetup::new(make_coords(&[1, 2, 0]), make_coords(&[3, 4, 0]))
            .with_flags(SessionFlags::new().with_ack().bidirectional())
            .with_handshake(handshake.clone());

        let encoded = setup.encode();

        // Verify FSP prefix: ver_phase=0x01 (version 0, phase MSG1)
        assert_eq!(encoded[0], 0x01);
        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake
        let payload_len = u16::from_le_bytes([encoded[2], encoded[3]]);
        assert_eq!(payload_len as usize, encoded.len() - 4);

        // Decode (skip 4-byte FSP prefix)
        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();

        assert_eq!(decoded.flags, setup.flags);
        assert_eq!(decoded.src_coords, setup.src_coords);
        assert_eq!(decoded.dest_coords, setup.dest_coords);
        assert_eq!(decoded.handshake_payload, handshake);
    }

    #[test]
    fn test_session_setup_no_handshake() {
        let setup = SessionSetup::new(make_coords(&[5, 0]), make_coords(&[6, 0]));

        let encoded = setup.encode();
        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();

        assert!(decoded.handshake_payload.is_empty());
        assert_eq!(decoded.src_coords, setup.src_coords);
        assert_eq!(decoded.dest_coords, setup.dest_coords);
    }

    #[test]
    fn test_session_ack_encode_decode() {
        let handshake = vec![0xBB; 33]; // typical Noise IK msg2
        let ack = SessionAck::new(make_coords(&[7, 8, 0]), make_coords(&[3, 4, 0]))
            .with_handshake(handshake.clone());

        let encoded = ack.encode();
        // Verify FSP prefix: ver_phase=0x02 (version 0, phase MSG2)
        assert_eq!(encoded[0], 0x02);
        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake

        let decoded = SessionAck::decode(&encoded[4..]).unwrap();
        assert_eq!(decoded.src_coords, ack.src_coords);
        assert_eq!(decoded.dest_coords, ack.dest_coords);
        assert_eq!(decoded.handshake_payload, handshake);
    }

    #[test]
    fn test_coords_required_encode_decode() {
        let err = CoordsRequired::new(make_node_addr(0xAA), make_node_addr(0xBB));

        let encoded = err.encode();
        // 4 prefix + 1 msg_type + 1 flags + 16 dest + 16 reporter = 38
        assert_eq!(encoded.len(), 4 + COORDS_REQUIRED_SIZE);
        // Check FSP prefix: phase 0x0, U flag
        assert_eq!(encoded[0], 0x00);
        assert_eq!(encoded[1], 0x04); // U flag
        // msg_type after prefix
        assert_eq!(encoded[4], 0x20);

        // decode after prefix + msg_type consumed
        let decoded = CoordsRequired::decode(&encoded[5..]).unwrap();
        assert_eq!(decoded.dest_addr, err.dest_addr);
        assert_eq!(decoded.reporter, err.reporter);
    }

    #[test]
    fn test_path_broken_encode_decode_no_coords() {
        let err = PathBroken::new(make_node_addr(0xCC), make_node_addr(0xDD));

        let encoded = err.encode();
        // Check FSP prefix
        assert_eq!(encoded[0], 0x00);
        assert_eq!(encoded[1], 0x04); // U flag
        assert_eq!(encoded[4], 0x21); // msg_type

        let decoded = PathBroken::decode(&encoded[5..]).unwrap();
        assert_eq!(decoded.dest_addr, err.dest_addr);
        assert_eq!(decoded.reporter, err.reporter);
        assert!(decoded.last_known_coords.is_none());
    }

    #[test]
    fn test_path_broken_encode_decode_with_coords() {
        let coords = make_coords(&[0xCC, 0xDD, 0xEE]);
        let err = PathBroken::new(make_node_addr(0x11), make_node_addr(0x22))
            .with_last_coords(coords.clone());

        let encoded = err.encode();
        let decoded = PathBroken::decode(&encoded[5..]).unwrap();

        assert_eq!(decoded.dest_addr, err.dest_addr);
        assert_eq!(decoded.reporter, err.reporter);
        assert_eq!(decoded.last_known_coords.unwrap(), coords);
    }

    #[test]
    fn test_session_setup_decode_too_short() {
        assert!(SessionSetup::decode(&[]).is_err());
    }

    #[test]
    fn test_session_ack_decode_too_short() {
        assert!(SessionAck::decode(&[]).is_err());
    }

    #[test]
    fn test_coords_required_decode_too_short() {
        assert!(CoordsRequired::decode(&[]).is_err());
        assert!(CoordsRequired::decode(&[0x00; 10]).is_err());
    }

    #[test]
    fn test_path_broken_decode_too_short() {
        assert!(PathBroken::decode(&[]).is_err());
        assert!(PathBroken::decode(&[0x00; 20]).is_err());
    }

    #[test]
    fn test_session_setup_deep_coords() {
        // Depth-10 coordinate (11 entries: self + 10 ancestors)
        let addrs: Vec<u8> = (0..11).collect();
        let src = make_coords(&addrs);
        let dest = make_coords(&[20, 21, 22, 23, 24]);
        let setup = SessionSetup::new(src.clone(), dest.clone()).with_handshake(vec![0x55; 82]);

        let encoded = setup.encode();
        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();

        assert_eq!(decoded.src_coords, src);
        assert_eq!(decoded.dest_coords, dest);
    }

    // ===== FspFlags Tests =====

    #[test]
    fn test_fsp_flags_default() {
        let flags = FspFlags::new();
        assert!(!flags.coords_present);
        assert!(!flags.key_epoch);
        assert!(!flags.unencrypted);
        assert_eq!(flags.to_byte(), 0x00);
    }

    #[test]
    fn test_fsp_flags_roundtrip() {
        // All combinations of 3 bits
        for byte in 0u8..=0x07 {
            let flags = FspFlags::from_byte(byte);
            assert_eq!(flags.to_byte(), byte);
        }
    }

    #[test]
    fn test_fsp_flags_individual_bits() {
        let cp = FspFlags::from_byte(0x01);
        assert!(cp.coords_present);
        assert!(!cp.key_epoch);
        assert!(!cp.unencrypted);

        let k = FspFlags::from_byte(0x02);
        assert!(!k.coords_present);
        assert!(k.key_epoch);
        assert!(!k.unencrypted);

        let u = FspFlags::from_byte(0x04);
        assert!(!u.coords_present);
        assert!(!u.key_epoch);
        assert!(u.unencrypted);
    }

    #[test]
    fn test_fsp_flags_ignores_reserved_bits() {
        // Reserved bits in upper 5 bits are not preserved
        let flags = FspFlags::from_byte(0xFF);
        assert!(flags.coords_present);
        assert!(flags.key_epoch);
        assert!(flags.unencrypted);
        assert_eq!(flags.to_byte(), 0x07); // only lower 3 bits
    }

    // ===== FspInnerFlags Tests =====

    #[test]
    fn test_fsp_inner_flags_default() {
        let flags = FspInnerFlags::new();
        assert!(!flags.spin_bit);
        assert_eq!(flags.to_byte(), 0x00);
    }

    #[test]
    fn test_fsp_inner_flags_roundtrip() {
        let flags = FspInnerFlags::from_byte(0x01);
        assert!(flags.spin_bit);
        assert_eq!(flags.to_byte(), 0x01);

        let flags = FspInnerFlags::from_byte(0x00);
        assert!(!flags.spin_bit);
        assert_eq!(flags.to_byte(), 0x00);
    }

    #[test]
    fn test_fsp_inner_flags_ignores_reserved() {
        let flags = FspInnerFlags::from_byte(0xFE);
        assert!(!flags.spin_bit);
        assert_eq!(flags.to_byte(), 0x00);

        let flags = FspInnerFlags::from_byte(0xFF);
        assert!(flags.spin_bit);
        assert_eq!(flags.to_byte(), 0x01);
    }

    // ===== New SessionMessageType Values =====

    #[test]
    fn test_session_message_type_new_values() {
        assert_eq!(SessionMessageType::SenderReport.to_byte(), 0x11);
        assert_eq!(SessionMessageType::ReceiverReport.to_byte(), 0x12);
        assert_eq!(SessionMessageType::PathMtuNotification.to_byte(), 0x13);
    }

    #[test]
    fn test_session_message_type_display() {
        assert_eq!(
            format!("{}", SessionMessageType::SenderReport),
            "SenderReport"
        );
        assert_eq!(
            format!("{}", SessionMessageType::ReceiverReport),
            "ReceiverReport"
        );
        assert_eq!(
            format!("{}", SessionMessageType::PathMtuNotification),
            "PathMtuNotification"
        );
    }

    // ===== SessionSenderReport Tests =====

    fn sample_session_sender_report() -> SessionSenderReport {
        SessionSenderReport {
            interval_start_counter: 100,
            interval_end_counter: 200,
            interval_start_timestamp: 5000,
            interval_end_timestamp: 6000,
            interval_bytes_sent: 50_000,
            cumulative_packets_sent: 10_000,
            cumulative_bytes_sent: 5_000_000,
        }
    }

    #[test]
    fn test_session_sender_report_encode_size() {
        let sr = sample_session_sender_report();
        let encoded = sr.encode();
        assert_eq!(encoded.len(), SESSION_SENDER_REPORT_SIZE);
    }

    #[test]
    fn test_session_sender_report_roundtrip() {
        let sr = sample_session_sender_report();
        let encoded = sr.encode();
        let decoded = SessionSenderReport::decode(&encoded).unwrap();
        assert_eq!(sr, decoded);
    }

    #[test]
    fn test_session_sender_report_too_short() {
        assert!(SessionSenderReport::decode(&[0u8; 10]).is_err());
    }

    // ===== SessionReceiverReport Tests =====

    fn sample_session_receiver_report() -> SessionReceiverReport {
        SessionReceiverReport {
            highest_counter: 195,
            cumulative_packets_recv: 9_500,
            cumulative_bytes_recv: 4_750_000,
            timestamp_echo: 5900,
            dwell_time: 5,
            max_burst_loss: 3,
            mean_burst_loss: 384,
            jitter: 1200,
            ecn_ce_count: 0,
            owd_trend: -50,
            burst_loss_count: 2,
            cumulative_reorder_count: 10,
            interval_packets_recv: 95,
            interval_bytes_recv: 47_500,
        }
    }

    #[test]
    fn test_session_receiver_report_encode_size() {
        let rr = sample_session_receiver_report();
        let encoded = rr.encode();
        assert_eq!(encoded.len(), SESSION_RECEIVER_REPORT_SIZE);
    }

    #[test]
    fn test_session_receiver_report_roundtrip() {
        let rr = sample_session_receiver_report();
        let encoded = rr.encode();
        let decoded = SessionReceiverReport::decode(&encoded).unwrap();
        assert_eq!(rr, decoded);
    }

    #[test]
    fn test_session_receiver_report_too_short() {
        assert!(SessionReceiverReport::decode(&[0u8; 10]).is_err());
    }

    #[test]
    fn test_session_receiver_report_negative_owd_trend() {
        let rr = SessionReceiverReport {
            owd_trend: -12345,
            ..sample_session_receiver_report()
        };
        let encoded = rr.encode();
        let decoded = SessionReceiverReport::decode(&encoded).unwrap();
        assert_eq!(decoded.owd_trend, -12345);
    }

    // ===== PathMtuNotification Tests =====

    #[test]
    fn test_path_mtu_notification_encode_size() {
        let n = PathMtuNotification::new(1400);
        let encoded = n.encode();
        assert_eq!(encoded.len(), PATH_MTU_NOTIFICATION_SIZE);
    }

    #[test]
    fn test_path_mtu_notification_roundtrip() {
        let n = PathMtuNotification::new(1400);
        let encoded = n.encode();
        let decoded = PathMtuNotification::decode(&encoded).unwrap();
        assert_eq!(decoded.path_mtu, 1400);
    }

    #[test]
    fn test_path_mtu_notification_too_short() {
        assert!(PathMtuNotification::decode(&[]).is_err());
        assert!(PathMtuNotification::decode(&[0x00]).is_err());
    }

    #[test]
    fn test_path_mtu_notification_boundary_values() {
        for mtu in [0u16, 1280, 1500, u16::MAX] {
            let n = PathMtuNotification::new(mtu);
            let encoded = n.encode();
            let decoded = PathMtuNotification::decode(&encoded).unwrap();
            assert_eq!(decoded.path_mtu, mtu);
        }
    }

    // ===== MtuExceeded Tests =====

    #[test]
    fn test_mtu_exceeded_encode_size() {
        let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);
        let encoded = err.encode();
        // 4 prefix + 36 body = 40
        assert_eq!(encoded.len(), 4 + MTU_EXCEEDED_SIZE);
    }

    #[test]
    fn test_mtu_exceeded_encode_decode() {
        let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);

        let encoded = err.encode();
        // Check FSP prefix: phase 0x0, U flag
        assert_eq!(encoded[0], 0x00);
        assert_eq!(encoded[1], 0x04); // U flag
        // msg_type after prefix
        assert_eq!(encoded[4], 0x22);

        // decode after prefix + msg_type consumed
        let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
        assert_eq!(decoded.dest_addr, err.dest_addr);
        assert_eq!(decoded.reporter, err.reporter);
        assert_eq!(decoded.mtu, 1400);
    }

    #[test]
    fn test_mtu_exceeded_decode_too_short() {
        assert!(MtuExceeded::decode(&[]).is_err());
        assert!(MtuExceeded::decode(&[0x00; 20]).is_err());
        assert!(MtuExceeded::decode(&[0x00; 34]).is_err()); // exactly 1 byte short
    }

    #[test]
    fn test_mtu_exceeded_boundary_mtu_values() {
        for mtu in [0u16, 1280, 1500, u16::MAX] {
            let err = MtuExceeded::new(make_node_addr(1), make_node_addr(2), mtu);
            let encoded = err.encode();
            let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
            assert_eq!(decoded.mtu, mtu);
        }
    }

    #[test]
    fn test_mtu_exceeded_message_type_value() {
        assert_eq!(SessionMessageType::MtuExceeded.to_byte(), 0x22);
        assert_eq!(
            SessionMessageType::from_byte(0x22),
            Some(SessionMessageType::MtuExceeded)
        );
    }

    #[test]
    fn test_mtu_exceeded_display() {
        assert_eq!(
            format!("{}", SessionMessageType::MtuExceeded),
            "MtuExceeded"
        );
    }

    // ===== SessionMsg3 Tests =====

    #[test]
    fn test_session_msg3_encode_decode() {
        let handshake = vec![0xCC; 73]; // typical XK msg3
        let msg3 = SessionMsg3::new(handshake.clone());

        let encoded = msg3.encode();
        // Verify FSP prefix: ver_phase=0x03 (version 0, phase MSG3)
        assert_eq!(encoded[0], 0x03);
        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake
        let payload_len = u16::from_le_bytes([encoded[2], encoded[3]]);
        assert_eq!(payload_len as usize, encoded.len() - 4);

        // Decode (skip 4-byte FSP prefix)
        let decoded = SessionMsg3::decode(&encoded[4..]).unwrap();
        assert_eq!(decoded.flags, 0);
        assert_eq!(decoded.handshake_payload, handshake);
    }

    #[test]
    fn test_session_msg3_decode_too_short() {
        assert!(SessionMsg3::decode(&[]).is_err());
        assert!(SessionMsg3::decode(&[0x00]).is_err()); // flags only, no hs_len
    }

    #[test]
    fn test_session_msg3_empty_handshake() {
        let msg3 = SessionMsg3::new(vec![]);
        let encoded = msg3.encode();
        let decoded = SessionMsg3::decode(&encoded[4..]).unwrap();
        assert!(decoded.handshake_payload.is_empty());
    }
}
