//! Link-layer message types: handshake, link control, disconnect, session datagram.

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

// ============================================================================
// Handshake Message Types
// ============================================================================

/// Handshake message type identifiers.
///
/// These messages are exchanged during Noise IK handshake before link
/// encryption is established. They use the same TLV framing as link
/// messages but payloads are not encrypted (except Noise-internal encryption).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum HandshakeMessageType {
    /// Noise IK message 1: initiator sends ephemeral + encrypted static.
    /// Payload: 82 bytes (33 ephemeral + 33 static + 16 tag).
    NoiseIKMsg1 = 0x01,

    /// Noise IK message 2: responder sends ephemeral.
    /// Payload: 33 bytes (ephemeral pubkey only).
    NoiseIKMsg2 = 0x02,
}

impl HandshakeMessageType {
    /// Try to convert from a byte.
    pub fn from_byte(b: u8) -> Option<Self> {
        match b {
            0x01 => Some(HandshakeMessageType::NoiseIKMsg1),
            0x02 => Some(HandshakeMessageType::NoiseIKMsg2),
            _ => None,
        }
    }

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

    /// Check if a byte represents a handshake message type.
    pub fn is_handshake(b: u8) -> bool {
        matches!(b, 0x01 | 0x02)
    }
}

impl fmt::Display for HandshakeMessageType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            HandshakeMessageType::NoiseIKMsg1 => "NoiseIKMsg1",
            HandshakeMessageType::NoiseIKMsg2 => "NoiseIKMsg2",
        };
        write!(f, "{}", name)
    }
}

// ============================================================================
// Link-Layer Message Types
// ============================================================================

/// Link-layer message type identifiers.
///
/// These messages are exchanged between directly connected peers over
/// Noise-encrypted links. All payloads are encrypted with session keys
/// established during the Noise IK handshake.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum LinkMessageType {
    // Forwarding (0x00-0x0F)
    /// Encapsulated session-layer datagram for forwarding.
    /// Payload is opaque to intermediate nodes (end-to-end encrypted).
    SessionDatagram = 0x00,

    // MMP reports (0x01-0x02) — content defined in TASK-2026-0006
    /// Sender-side MMP report (stub).
    SenderReport = 0x01,
    /// Receiver-side MMP report (stub).
    ReceiverReport = 0x02,

    // Tree protocol (0x10-0x1F)
    /// Spanning tree state announcement.
    TreeAnnounce = 0x10,

    // Bloom filter (0x20-0x2F)
    /// Bloom filter reachability update.
    FilterAnnounce = 0x20,

    // Discovery (0x30-0x3F)
    /// Request to discover a node's coordinates.
    LookupRequest = 0x30,
    /// Response with target's coordinates.
    LookupResponse = 0x31,

    // Link Control (0x50-0x5F)
    /// Orderly disconnect notification before link closure.
    Disconnect = 0x50,
    /// Periodic heartbeat for link liveness detection.
    /// No payload — the msg_type byte alone is sufficient.
    Heartbeat = 0x51,
}

impl LinkMessageType {
    /// Try to convert from a byte.
    pub fn from_byte(b: u8) -> Option<Self> {
        match b {
            0x00 => Some(LinkMessageType::SessionDatagram),
            0x01 => Some(LinkMessageType::SenderReport),
            0x02 => Some(LinkMessageType::ReceiverReport),
            0x10 => Some(LinkMessageType::TreeAnnounce),
            0x20 => Some(LinkMessageType::FilterAnnounce),
            0x30 => Some(LinkMessageType::LookupRequest),
            0x31 => Some(LinkMessageType::LookupResponse),
            0x50 => Some(LinkMessageType::Disconnect),
            0x51 => Some(LinkMessageType::Heartbeat),
            _ => None,
        }
    }

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

impl fmt::Display for LinkMessageType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            LinkMessageType::SessionDatagram => "SessionDatagram",
            LinkMessageType::SenderReport => "SenderReport",
            LinkMessageType::ReceiverReport => "ReceiverReport",
            LinkMessageType::TreeAnnounce => "TreeAnnounce",
            LinkMessageType::FilterAnnounce => "FilterAnnounce",
            LinkMessageType::LookupRequest => "LookupRequest",
            LinkMessageType::LookupResponse => "LookupResponse",
            LinkMessageType::Disconnect => "Disconnect",
            LinkMessageType::Heartbeat => "Heartbeat",
        };
        write!(f, "{}", name)
    }
}

// ============================================================================
// Disconnect Reason Codes
// ============================================================================

/// Reason for an orderly disconnect notification.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DisconnectReason {
    /// Normal shutdown (operator requested).
    Shutdown = 0x00,
    /// Restarting (may reconnect soon).
    Restart = 0x01,
    /// Protocol error encountered.
    ProtocolError = 0x02,
    /// Transport failure.
    TransportFailure = 0x03,
    /// Resource exhaustion (memory, connections).
    ResourceExhaustion = 0x04,
    /// Authentication or security policy violation.
    SecurityViolation = 0x05,
    /// Configuration change (peer removed from config).
    ConfigurationChange = 0x06,
    /// Timeout or keepalive failure.
    Timeout = 0x07,
    /// Unspecified reason.
    Other = 0xFF,
}

impl DisconnectReason {
    /// Try to convert from a byte.
    pub fn from_byte(b: u8) -> Option<Self> {
        match b {
            0x00 => Some(DisconnectReason::Shutdown),
            0x01 => Some(DisconnectReason::Restart),
            0x02 => Some(DisconnectReason::ProtocolError),
            0x03 => Some(DisconnectReason::TransportFailure),
            0x04 => Some(DisconnectReason::ResourceExhaustion),
            0x05 => Some(DisconnectReason::SecurityViolation),
            0x06 => Some(DisconnectReason::ConfigurationChange),
            0x07 => Some(DisconnectReason::Timeout),
            0xFF => Some(DisconnectReason::Other),
            _ => None,
        }
    }

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

impl fmt::Display for DisconnectReason {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            DisconnectReason::Shutdown => "Shutdown",
            DisconnectReason::Restart => "Restart",
            DisconnectReason::ProtocolError => "ProtocolError",
            DisconnectReason::TransportFailure => "TransportFailure",
            DisconnectReason::ResourceExhaustion => "ResourceExhaustion",
            DisconnectReason::SecurityViolation => "SecurityViolation",
            DisconnectReason::ConfigurationChange => "ConfigurationChange",
            DisconnectReason::Timeout => "Timeout",
            DisconnectReason::Other => "Other",
        };
        write!(f, "{}", name)
    }
}

// ============================================================================
// Disconnect Message
// ============================================================================

/// Orderly disconnect notification sent before closing a peer link.
///
/// Sent as a link-layer message (type 0x50) inside an encrypted frame.
/// Allows the receiving peer to immediately clean up state rather than
/// waiting for timeout-based detection.
///
/// ## Wire Format
///
/// | Offset | Field    | Size   | Notes                  |
/// |--------|----------|--------|------------------------|
/// | 0      | msg_type | 1 byte | 0x50                   |
/// | 1      | reason   | 1 byte | DisconnectReason value |
#[derive(Clone, Debug)]
pub struct Disconnect {
    /// Reason for disconnection.
    pub reason: DisconnectReason,
}

impl Disconnect {
    /// Create a new Disconnect message.
    pub fn new(reason: DisconnectReason) -> Self {
        Self { reason }
    }

    /// Encode as link-layer plaintext (msg_type + reason).
    pub fn encode(&self) -> [u8; 2] {
        [LinkMessageType::Disconnect.to_byte(), self.reason.to_byte()]
    }

    /// Decode from link-layer payload (after msg_type byte has been consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        if payload.is_empty() {
            return Err(ProtocolError::MessageTooShort {
                expected: 1,
                got: 0,
            });
        }
        let reason = DisconnectReason::from_byte(payload[0]).unwrap_or(DisconnectReason::Other);
        Ok(Self { reason })
    }
}

// ============================================================================
// Session Datagram (Link-Layer Encapsulation)
// ============================================================================

/// Encapsulated session-layer datagram for multi-hop forwarding.
///
/// This is a link-layer message (type 0x00) that carries session-layer
/// payloads through the mesh. The envelope provides source and destination
/// addressing that transit routers use for forwarding decisions and error
/// routing.
///
/// ## Wire Format (36-byte fixed header)
///
/// | Offset | Field     | Size     | Description                         |
/// |--------|-----------|----------|-------------------------------------|
/// | 0      | msg_type  | 1 byte   | 0x00                                |
/// | 1      | ttl       | 1 byte   | Decremented each hop                |
/// | 2      | path_mtu  | 2 bytes  | Path MTU (LE), min'd at each hop    |
/// | 4      | src_addr  | 16 bytes | Source node_addr                    |
/// | 20     | dest_addr | 16 bytes | Destination node_addr               |
/// | 36     | payload   | variable | Session-layer message               |
///
/// The payload is either end-to-end encrypted (handshake messages, data,
/// reports) or plaintext error signals (CoordsRequired, PathBroken)
/// generated by transit routers.
#[derive(Clone, Debug)]
pub struct SessionDatagram {
    /// Source node address (originator of this datagram).
    /// For data traffic: the source endpoint.
    /// For error signals: the transit router that generated the error.
    pub src_addr: NodeAddr,
    /// Destination node address (for routing decisions).
    pub dest_addr: NodeAddr,
    /// Time-to-live (decremented at each hop, dropped at zero).
    pub ttl: u8,
    /// Path MTU: minimum link MTU along the path so far.
    /// Each forwarding hop applies min(path_mtu, outgoing_link_mtu).
    pub path_mtu: u16,
    /// Session-layer payload (e2e encrypted or plaintext error signal).
    pub payload: Vec<u8>,
}

/// Borrowed view of a session datagram payload.
///
/// This avoids allocating and copying the inner payload when the caller only
/// needs to inspect or locally deliver it.
#[derive(Clone, Copy, Debug)]
pub struct SessionDatagramRef<'a> {
    pub src_addr: NodeAddr,
    pub dest_addr: NodeAddr,
    pub ttl: u8,
    pub path_mtu: u16,
    pub payload: &'a [u8],
}

/// SessionDatagram fixed header size: msg_type(1) + ttl(1) + path_mtu(2) + src_addr(16) + dest_addr(16).
pub const SESSION_DATAGRAM_HEADER_SIZE: usize = 36;

impl SessionDatagram {
    /// Create a new session datagram.
    pub fn new(src_addr: NodeAddr, dest_addr: NodeAddr, payload: Vec<u8>) -> Self {
        Self {
            src_addr,
            dest_addr,
            ttl: 64,
            path_mtu: u16::MAX,
            payload,
        }
    }

    /// Set the TTL.
    pub fn with_ttl(mut self, ttl: u8) -> Self {
        self.ttl = ttl;
        self
    }

    /// Set the path MTU.
    pub fn with_path_mtu(mut self, path_mtu: u16) -> Self {
        self.path_mtu = path_mtu;
        self
    }

    /// Decrement the TTL for a transit hop, returning whether the result may
    /// still be transmitted.
    ///
    /// Follows IP semantics: the decrement happens first, and a datagram that
    /// would leave with a TTL of zero is not transmitted. `saturating_sub`
    /// folds an already-exhausted arrival (TTL 0) into the same outcome as a
    /// last-hop arrival (TTL 1); both leave `ttl` at 0 and return false.
    ///
    /// This governs forwarding only. Delivery to the addressed node is not
    /// TTL-gated and must not consult this method.
    pub fn decrement_ttl(&mut self) -> bool {
        self.ttl = self.ttl.saturating_sub(1);
        self.ttl > 0
    }

    /// Check whether this datagram would survive a transit hop.
    ///
    /// True only at TTL 2 or more: at TTL 1 the decrement leaves zero, so the
    /// datagram is dropped rather than forwarded.
    pub fn can_forward(&self) -> bool {
        self.ttl > 1
    }

    /// Encode as link-layer message (msg_type + ttl + path_mtu + src_addr + dest_addr + payload).
    pub fn encode(&self) -> Vec<u8> {
        let mut buf = Vec::with_capacity(SESSION_DATAGRAM_HEADER_SIZE + self.payload.len());
        buf.push(LinkMessageType::SessionDatagram.to_byte());
        buf.push(self.ttl);
        buf.extend_from_slice(&self.path_mtu.to_le_bytes());
        buf.extend_from_slice(self.src_addr.as_bytes());
        buf.extend_from_slice(self.dest_addr.as_bytes());
        buf.extend_from_slice(&self.payload);
        buf
    }

    /// Decode from link-layer payload (after msg_type byte has been consumed).
    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
        let view = SessionDatagramRef::decode(payload)?;
        Ok(view.into_owned())
    }
}

impl<'a> SessionDatagramRef<'a> {
    /// Decode a borrowed view from link-layer payload after the msg_type byte.
    pub fn decode(payload: &'a [u8]) -> Result<Self, ProtocolError> {
        // ttl(1) + path_mtu(2) + src_addr(16) + dest_addr(16) = 35
        if payload.len() < 35 {
            return Err(ProtocolError::MessageTooShort {
                expected: 35,
                got: payload.len(),
            });
        }
        let ttl = payload[0];
        let path_mtu = u16::from_le_bytes([payload[1], payload[2]]);
        let mut src_bytes = [0u8; 16];
        src_bytes.copy_from_slice(&payload[3..19]);
        let mut dest_bytes = [0u8; 16];
        dest_bytes.copy_from_slice(&payload[19..35]);

        Ok(Self {
            src_addr: NodeAddr::from_bytes(src_bytes),
            dest_addr: NodeAddr::from_bytes(dest_bytes),
            ttl,
            path_mtu,
            payload: &payload[35..],
        })
    }

    /// Materialize an owned datagram for forwarding/re-encoding paths.
    pub fn into_owned(self) -> SessionDatagram {
        SessionDatagram {
            src_addr: self.src_addr,
            dest_addr: self.dest_addr,
            ttl: self.ttl,
            path_mtu: self.path_mtu,
            payload: self.payload.to_vec(),
        }
    }
}

// Legacy type alias for compatibility during transition
#[deprecated(note = "Use LinkMessageType or SessionMessageType instead")]
pub type MessageType = LinkMessageType;

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

    // ===== HandshakeMessageType Tests =====

    #[test]
    fn test_handshake_message_type_roundtrip() {
        let types = [
            HandshakeMessageType::NoiseIKMsg1,
            HandshakeMessageType::NoiseIKMsg2,
        ];

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

    #[test]
    fn test_handshake_message_type_invalid() {
        assert!(HandshakeMessageType::from_byte(0x00).is_none());
        assert!(HandshakeMessageType::from_byte(0x03).is_none());
        assert!(HandshakeMessageType::from_byte(0x10).is_none());
    }

    #[test]
    fn test_handshake_message_type_is_handshake() {
        assert!(HandshakeMessageType::is_handshake(0x01));
        assert!(HandshakeMessageType::is_handshake(0x02));
        assert!(!HandshakeMessageType::is_handshake(0x00));
        assert!(!HandshakeMessageType::is_handshake(0x10));
    }

    // ===== LinkMessageType Tests =====

    #[test]
    fn test_link_message_type_roundtrip() {
        let types = [
            LinkMessageType::TreeAnnounce,
            LinkMessageType::FilterAnnounce,
            LinkMessageType::LookupRequest,
            LinkMessageType::LookupResponse,
            LinkMessageType::SessionDatagram,
            LinkMessageType::Disconnect,
            LinkMessageType::Heartbeat,
        ];

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

    #[test]
    fn test_link_message_type_invalid() {
        assert!(LinkMessageType::from_byte(0xFF).is_none());
        assert!(LinkMessageType::from_byte(0x03).is_none());
        assert!(LinkMessageType::from_byte(0x40).is_none());
    }

    // ===== DisconnectReason Tests =====

    #[test]
    fn test_disconnect_reason_roundtrip() {
        let reasons = [
            DisconnectReason::Shutdown,
            DisconnectReason::Restart,
            DisconnectReason::ProtocolError,
            DisconnectReason::TransportFailure,
            DisconnectReason::ResourceExhaustion,
            DisconnectReason::SecurityViolation,
            DisconnectReason::ConfigurationChange,
            DisconnectReason::Timeout,
            DisconnectReason::Other,
        ];

        for reason in reasons {
            let byte = reason.to_byte();
            let restored = DisconnectReason::from_byte(byte);
            assert_eq!(restored, Some(reason));
        }
    }

    #[test]
    fn test_disconnect_reason_unknown_byte() {
        assert!(DisconnectReason::from_byte(0x08).is_none());
        assert!(DisconnectReason::from_byte(0x80).is_none());
        assert!(DisconnectReason::from_byte(0xFE).is_none());
    }

    // ===== Disconnect Message Tests =====

    #[test]
    fn test_disconnect_encode_decode() {
        let msg = Disconnect::new(DisconnectReason::Shutdown);
        let encoded = msg.encode();

        assert_eq!(encoded.len(), 2);
        assert_eq!(encoded[0], 0x50); // LinkMessageType::Disconnect
        assert_eq!(encoded[1], 0x00); // DisconnectReason::Shutdown

        // Decode from payload (after msg_type byte)
        let decoded = Disconnect::decode(&encoded[1..]).unwrap();
        assert_eq!(decoded.reason, DisconnectReason::Shutdown);
    }

    #[test]
    fn test_disconnect_all_reasons() {
        let reasons = [
            DisconnectReason::Shutdown,
            DisconnectReason::Restart,
            DisconnectReason::ProtocolError,
            DisconnectReason::Other,
        ];

        for reason in reasons {
            let msg = Disconnect::new(reason);
            let encoded = msg.encode();
            let decoded = Disconnect::decode(&encoded[1..]).unwrap();
            assert_eq!(decoded.reason, reason);
        }
    }

    #[test]
    fn test_disconnect_decode_empty_payload() {
        let result = Disconnect::decode(&[]);
        assert!(result.is_err());
    }

    #[test]
    fn test_disconnect_decode_unknown_reason() {
        let decoded = Disconnect::decode(&[0x80]).unwrap();
        assert_eq!(decoded.reason, DisconnectReason::Other);
    }

    // ===== SessionDatagram Tests =====

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

    #[test]
    fn test_session_datagram_encode_decode() {
        let src = make_node_addr(0xAA);
        let dest = make_node_addr(0xBB);
        let payload = vec![0x10, 0x00, 0x05, 0x00, 1, 2, 3, 4, 5]; // session payload
        let dg = SessionDatagram::new(src, dest, payload.clone()).with_ttl(32);

        let encoded = dg.encode();
        assert_eq!(encoded[0], 0x00); // msg_type (SessionDatagram)
        assert_eq!(encoded.len(), SESSION_DATAGRAM_HEADER_SIZE + payload.len());

        // Decode (after msg_type)
        let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
        assert_eq!(decoded.src_addr, src);
        assert_eq!(decoded.dest_addr, dest);
        assert_eq!(decoded.ttl, 32);
        assert_eq!(decoded.payload, payload);
    }

    #[test]
    fn test_session_datagram_ref_decode_borrows_payload() {
        let src = make_node_addr(0xAA);
        let dest = make_node_addr(0xBB);
        let payload = vec![0x10, 0x00, 0x05, 0x00, 1, 2, 3, 4, 5];
        let dg = SessionDatagram::new(src, dest, payload.clone())
            .with_ttl(32)
            .with_path_mtu(1400);

        let encoded = dg.encode();
        let decoded = SessionDatagramRef::decode(&encoded[1..]).unwrap();

        assert_eq!(decoded.src_addr, src);
        assert_eq!(decoded.dest_addr, dest);
        assert_eq!(decoded.ttl, 32);
        assert_eq!(decoded.path_mtu, 1400);
        assert_eq!(decoded.payload, payload.as_slice());
        assert_eq!(
            decoded.payload.as_ptr(),
            encoded[SESSION_DATAGRAM_HEADER_SIZE..].as_ptr()
        );
    }

    #[test]
    #[ignore = "performance benchmark; run explicitly with --ignored --nocapture"]
    fn bench_session_datagram_decode_owned_vs_ref() {
        use std::hint::black_box;
        use std::time::Instant;

        const ITERS: usize = 300_000;

        let src = make_node_addr(0xAA);
        let dest = make_node_addr(0xBB);
        let payload = vec![0x5A; 1200];
        let datagram = SessionDatagram::new(src, dest, payload)
            .with_ttl(32)
            .with_path_mtu(1400);
        let encoded = datagram.encode();
        let link_payload = &encoded[1..];

        let ref_start = Instant::now();
        let mut ref_bytes = 0usize;
        for _ in 0..ITERS {
            let decoded = SessionDatagramRef::decode(black_box(link_payload)).unwrap();
            ref_bytes = ref_bytes.wrapping_add(decoded.payload.len());
            black_box(decoded);
        }
        let ref_elapsed = ref_start.elapsed();

        let owned_start = Instant::now();
        let mut owned_bytes = 0usize;
        for _ in 0..ITERS {
            let decoded = SessionDatagram::decode(black_box(link_payload)).unwrap();
            owned_bytes = owned_bytes.wrapping_add(decoded.payload.len());
            black_box(decoded);
        }
        let owned_elapsed = owned_start.elapsed();

        assert_eq!(ref_bytes, owned_bytes);
        println!(
            "SessionDatagram decode: ref={:.1} ns/op owned={:.1} ns/op speedup={:.2}x iters={} payload_bytes={}",
            ref_elapsed.as_secs_f64() * 1_000_000_000.0 / ITERS as f64,
            owned_elapsed.as_secs_f64() * 1_000_000_000.0 / ITERS as f64,
            owned_elapsed.as_secs_f64() / ref_elapsed.as_secs_f64(),
            ITERS,
            link_payload.len() - 35
        );
    }

    #[test]
    fn test_session_datagram_empty_payload() {
        let dg = SessionDatagram::new(make_node_addr(1), make_node_addr(2), Vec::new());

        let encoded = dg.encode();
        assert_eq!(encoded.len(), SESSION_DATAGRAM_HEADER_SIZE);

        let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
        assert!(decoded.payload.is_empty());
    }

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

    #[test]
    fn test_session_datagram_ttl_roundtrip() {
        for hop in [0u8, 1, 64, 128, 255] {
            let dg = SessionDatagram::new(make_node_addr(1), make_node_addr(2), vec![0x42])
                .with_ttl(hop);

            let encoded = dg.encode();
            let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
            assert_eq!(decoded.ttl, hop);
        }
    }

    #[test]
    fn test_session_datagram_can_forward() {
        let dg = SessionDatagram::new(make_node_addr(1), make_node_addr(2), vec![0x42]);

        assert!(!dg.clone().with_ttl(0).can_forward());
        assert!(
            !dg.clone().with_ttl(1).can_forward(),
            "ttl=1 would leave at zero, so it is not forwardable"
        );
        assert!(
            dg.clone().with_ttl(2).can_forward(),
            "ttl=2 leaves at one, so it is forwardable"
        );
        assert!(dg.with_ttl(255).can_forward());
    }

    #[test]
    fn test_session_datagram_decrement_ttl() {
        let base = SessionDatagram::new(make_node_addr(1), make_node_addr(2), vec![0x42]);

        let mut dg = base.clone().with_ttl(0);
        assert!(!dg.decrement_ttl(), "ttl=0 is already exhausted");
        assert_eq!(dg.ttl, 0, "decrement must saturate rather than wrap");

        let mut dg = base.clone().with_ttl(1);
        assert!(
            !dg.decrement_ttl(),
            "ttl=1 leaves at zero, so it is dropped"
        );
        assert_eq!(dg.ttl, 0);

        let mut dg = base.clone().with_ttl(2);
        assert!(dg.decrement_ttl());
        assert_eq!(dg.ttl, 1);

        let mut dg = base.with_ttl(64);
        assert!(dg.decrement_ttl());
        assert_eq!(dg.ttl, 63);
    }
}
