//! Device-to-host messages (`PROTOCOL.md §6.7–6.10`).
//!
//! Four wire types:
//!
//! | Hex  | Name    | Tag                    | Notes                                 |
//! |------|---------|------------------------|---------------------------------------|
//! | 0x80 | OK      | echoes command         | Payload shape depends on command      |
//! | 0x81 | ERR     | command or 0x0000      | 2-byte error code                     |
//! | 0xC0 | RX      | always 0x0000          | Rich metadata + packet bytes          |
//! | 0xC1 | TX_DONE | echoes originating TX  | Result + airtime                      |
//!
//! OK is the only tricky one: its payload shape varies by what command
//! produced it, so callers must supply the originating command's type
//! when parsing (they know this from tag correlation). The three shapes
//! are empty (for `PING` / `TX` / `RX_START` / `RX_STOP`), `Info` (for
//! `GET_INFO`), and `SetConfigResult` (for `SET_CONFIG`).

use heapless::Vec as HVec;

use crate::{
    DeviceMessageEncodeError, DeviceMessageParseError, ErrorCode, Info, InfoParseError,
    MAX_OTA_PAYLOAD, Modulation, ModulationEncodeError, ModulationParseError, commands,
};

// ── D→H message type identifiers ────────────────────────────────────

pub const TYPE_OK: u8 = 0x80;
pub const TYPE_ERR: u8 = 0x81;
pub const TYPE_RX: u8 = 0xC0;
pub const TYPE_TX_DONE: u8 = 0xC1;

// ── OK sub-shapes ───────────────────────────────────────────────────

/// `SET_CONFIG` result code (`PROTOCOL.md §6.3`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum SetConfigResultCode {
    /// The request was accepted. The radio is now configured as requested.
    Applied = 0,
    /// Another client holds the lock; active config byte-for-byte matches.
    AlreadyMatched = 1,
    /// Another client holds the lock; active config differs.
    LockedMismatch = 2,
}

impl SetConfigResultCode {
    pub const fn as_u8(self) -> u8 {
        self as u8
    }
    pub const fn from_u8(v: u8) -> Option<Self> {
        Some(match v {
            0 => Self::Applied,
            1 => Self::AlreadyMatched,
            2 => Self::LockedMismatch,
            _ => return None,
        })
    }
}

/// `SET_CONFIG` lock owner (`PROTOCOL.md §6.3`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Owner {
    /// No client currently holds the lock.
    None = 0,
    /// The calling client holds the lock.
    Mine = 1,
    /// A different client holds the lock.
    Other = 2,
}

impl Owner {
    pub const fn as_u8(self) -> u8 {
        self as u8
    }
    pub const fn from_u8(v: u8) -> Option<Self> {
        Some(match v {
            0 => Self::None,
            1 => Self::Mine,
            2 => Self::Other,
            _ => return None,
        })
    }
}

/// Payload of `OK` in response to `SET_CONFIG` (`PROTOCOL.md §6.3`).
///
/// `current_modulation` and `current_params` always reflect what is
/// actually programmed into the radio at the moment the response was
/// constructed, regardless of whether this call caused the change.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct SetConfigResult {
    pub result: SetConfigResultCode,
    pub owner: Owner,
    pub current: Modulation,
}

impl SetConfigResult {
    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
        if buf.len() < 2 {
            return Err(DeviceMessageEncodeError::BufferTooSmall);
        }
        buf[0] = self.result.as_u8();
        buf[1] = self.owner.as_u8();
        let rest = self.current.encode(&mut buf[2..]).map_err(|e| match e {
            ModulationEncodeError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
            ModulationEncodeError::SyncWordTooLong => DeviceMessageEncodeError::SyncWordTooLong,
        })?;
        Ok(2 + rest)
    }

    pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
        if buf.len() < 2 {
            return Err(DeviceMessageParseError::TooShort);
        }
        let result =
            SetConfigResultCode::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
        let owner = Owner::from_u8(buf[1]).ok_or(DeviceMessageParseError::InvalidField)?;
        let current = Modulation::decode(&buf[2..]).map_err(DeviceMessageParseError::from)?;
        Ok(Self {
            result,
            owner,
            current,
        })
    }
}

/// Payload shape carried by an `OK` frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OkPayload {
    /// For `PING`, `TX`, `RX_START`, `RX_STOP` — no body.
    Empty,
    /// For `GET_INFO`.
    Info(Info),
    /// For `SET_CONFIG`.
    SetConfig(SetConfigResult),
}

impl OkPayload {
    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
        match self {
            Self::Empty => Ok(0),
            Self::Info(info) => info.encode(buf).map_err(|e| match e {
                InfoParseError::TooShort | InfoParseError::InvalidField => {
                    DeviceMessageEncodeError::InvalidField
                }
                InfoParseError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
            }),
            Self::SetConfig(r) => r.encode(buf),
        }
    }

    /// Parse the `OK` payload given the originating command's type byte.
    /// Returns `Empty` for `PING`/`TX`/`RX_START`/`RX_STOP` (and verifies
    /// that the payload is empty), `Info` for `GET_INFO`, `SetConfig` for
    /// `SET_CONFIG`. Any other `cmd_type` is rejected.
    pub fn parse_for(cmd_type: u8, payload: &[u8]) -> Result<Self, DeviceMessageParseError> {
        match cmd_type {
            commands::TYPE_PING
            | commands::TYPE_TX
            | commands::TYPE_RX_START
            | commands::TYPE_RX_STOP => {
                if !payload.is_empty() {
                    return Err(DeviceMessageParseError::WrongLength);
                }
                Ok(Self::Empty)
            }
            commands::TYPE_GET_INFO => Info::decode(payload)
                .map(Self::Info)
                .map_err(DeviceMessageParseError::from),
            commands::TYPE_SET_CONFIG => SetConfigResult::decode(payload).map(Self::SetConfig),
            _ => Err(DeviceMessageParseError::UnknownContext),
        }
    }
}

// ── RX event ────────────────────────────────────────────────────────

/// Packet origin — over-the-air vs. local-loopback from another client
/// (`PROTOCOL.md §6.9`, §13.4). In single-client firmware always `Ota`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum RxOrigin {
    Ota = 0,
    LocalLoopback = 1,
}

impl RxOrigin {
    pub const fn as_u8(self) -> u8 {
        self as u8
    }
    pub const fn from_u8(v: u8) -> Option<Self> {
        Some(match v {
            0 => Self::Ota,
            1 => Self::LocalLoopback,
            _ => return None,
        })
    }
}

/// `RX` event payload (`PROTOCOL.md §6.9`). Metadata is the 20-byte
/// prefix; `data` is the OTA packet body (0..=MAX_OTA_PAYLOAD bytes).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct RxPayload {
    /// RSSI in tenths of a dBm.
    pub rssi_tenths_dbm: i16,
    /// SNR in tenths of a dB.
    pub snr_tenths_db: i16,
    /// Estimated LO offset in Hz.
    pub freq_err_hz: i32,
    /// Microseconds since boot, captured at RxDone IRQ.
    pub timestamp_us: u64,
    /// True if the radio reported CRC pass (or CRC was not configured).
    pub crc_valid: bool,
    /// Packets dropped since the previous delivered RX. Cleared to 0 on
    /// every delivered event.
    pub packets_dropped: u16,
    pub origin: RxOrigin,
    pub data: HVec<u8, MAX_OTA_PAYLOAD>,
}

impl RxPayload {
    /// Fixed part of the payload preceding the variable-length `data`.
    pub const METADATA_SIZE: usize = 20;

    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
        let total = Self::METADATA_SIZE + self.data.len();
        if buf.len() < total {
            return Err(DeviceMessageEncodeError::BufferTooSmall);
        }
        if self.data.len() > MAX_OTA_PAYLOAD {
            return Err(DeviceMessageEncodeError::PayloadTooLarge);
        }
        buf[0..2].copy_from_slice(&self.rssi_tenths_dbm.to_le_bytes());
        buf[2..4].copy_from_slice(&self.snr_tenths_db.to_le_bytes());
        buf[4..8].copy_from_slice(&self.freq_err_hz.to_le_bytes());
        buf[8..16].copy_from_slice(&self.timestamp_us.to_le_bytes());
        buf[16] = u8::from(self.crc_valid);
        buf[17..19].copy_from_slice(&self.packets_dropped.to_le_bytes());
        buf[19] = self.origin.as_u8();
        buf[20..total].copy_from_slice(&self.data);
        Ok(total)
    }

    pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
        if buf.len() < Self::METADATA_SIZE {
            return Err(DeviceMessageParseError::TooShort);
        }
        let n = buf.len() - Self::METADATA_SIZE;
        if n > MAX_OTA_PAYLOAD {
            return Err(DeviceMessageParseError::WrongLength);
        }
        let rssi_tenths_dbm = i16::from_le_bytes([buf[0], buf[1]]);
        let snr_tenths_db = i16::from_le_bytes([buf[2], buf[3]]);
        let freq_err_hz = i32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
        let timestamp_us = u64::from_le_bytes([
            buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
        ]);
        let crc_valid = match buf[16] {
            0 => false,
            1 => true,
            _ => return Err(DeviceMessageParseError::InvalidField),
        };
        let packets_dropped = u16::from_le_bytes([buf[17], buf[18]]);
        let origin = RxOrigin::from_u8(buf[19]).ok_or(DeviceMessageParseError::InvalidField)?;
        // Slice with `n` rather than `..` so the subtraction `buf.len() -
        // METADATA_SIZE` is load-bearing: a mutant that replaces `-` with
        // `/` yields a different `n` and therefore a shorter data slice,
        // which round-trip tests observe.
        let mut data = HVec::new();
        data.extend_from_slice(&buf[Self::METADATA_SIZE..Self::METADATA_SIZE + n])
            .map_err(|_| DeviceMessageParseError::WrongLength)?;
        Ok(Self {
            rssi_tenths_dbm,
            snr_tenths_db,
            freq_err_hz,
            timestamp_us,
            crc_valid,
            packets_dropped,
            origin,
            data,
        })
    }
}

// ── TX_DONE event ───────────────────────────────────────────────────

/// `TX_DONE` result code (`PROTOCOL.md §6.10`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum TxResult {
    Transmitted = 0,
    ChannelBusy = 1,
    Cancelled = 2,
}

impl TxResult {
    pub const fn as_u8(self) -> u8 {
        self as u8
    }
    pub const fn from_u8(v: u8) -> Option<Self> {
        Some(match v {
            0 => Self::Transmitted,
            1 => Self::ChannelBusy,
            2 => Self::Cancelled,
            _ => return None,
        })
    }
}

/// `TX_DONE` payload (`PROTOCOL.md §6.10`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TxDonePayload {
    pub result: TxResult,
    /// Measured or computed on-air duration in microseconds. Always 0
    /// when `result` is not `Transmitted`.
    pub airtime_us: u32,
}

impl TxDonePayload {
    pub const WIRE_SIZE: usize = 5;

    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
        if buf.len() < Self::WIRE_SIZE {
            return Err(DeviceMessageEncodeError::BufferTooSmall);
        }
        buf[0] = self.result.as_u8();
        buf[1..5].copy_from_slice(&self.airtime_us.to_le_bytes());
        Ok(Self::WIRE_SIZE)
    }

    pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
        if buf.len() != Self::WIRE_SIZE {
            return Err(DeviceMessageParseError::WrongLength);
        }
        let result = TxResult::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
        let airtime_us = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
        Ok(Self { result, airtime_us })
    }
}

// ── ERR payload helper ──────────────────────────────────────────────

/// Encode an `ERR` payload (2 bytes of u16 LE error code).
pub fn encode_err_payload(
    code: ErrorCode,
    buf: &mut [u8],
) -> Result<usize, DeviceMessageEncodeError> {
    if buf.len() < 2 {
        return Err(DeviceMessageEncodeError::BufferTooSmall);
    }
    buf[0..2].copy_from_slice(&code.as_u16().to_le_bytes());
    Ok(2)
}

/// Decode an `ERR` payload (2 bytes).
pub fn decode_err_payload(buf: &[u8]) -> Result<ErrorCode, DeviceMessageParseError> {
    if buf.len() != 2 {
        return Err(DeviceMessageParseError::WrongLength);
    }
    Ok(ErrorCode::from_u16(u16::from_le_bytes([buf[0], buf[1]])))
}

// ── DeviceMessage top-level sum ────────────────────────────────────

/// Any device→host message, parsed into its semantic form.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DeviceMessage {
    Ok(OkPayload),
    Err(ErrorCode),
    Rx(RxPayload),
    TxDone(TxDonePayload),
}

impl DeviceMessage {
    pub const fn type_id(&self) -> u8 {
        match self {
            Self::Ok(_) => TYPE_OK,
            Self::Err(_) => TYPE_ERR,
            Self::Rx(_) => TYPE_RX,
            Self::TxDone(_) => TYPE_TX_DONE,
        }
    }

    /// Encode the device-message payload (bytes between frame header and
    /// CRC) into `buf`. Returns the number of bytes written.
    pub fn encode_payload(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
        match self {
            Self::Ok(ok) => ok.encode(buf),
            Self::Err(code) => encode_err_payload(*code, buf),
            Self::Rx(rx) => rx.encode(buf),
            Self::TxDone(td) => td.encode(buf),
        }
    }

    /// Parse a (`type_id`, `payload`) pair.
    ///
    /// `originating_cmd_type` MUST be supplied for `OK` frames: it is
    /// the `type_id` of the H→D command whose tag the host observed
    /// echoed back. For all other device-message types it is ignored
    /// (pass `None`).
    pub fn parse(
        type_id: u8,
        payload: &[u8],
        originating_cmd_type: Option<u8>,
    ) -> Result<Self, DeviceMessageParseError> {
        match type_id {
            TYPE_OK => {
                let cmd = originating_cmd_type.ok_or(DeviceMessageParseError::MissingContext)?;
                Ok(Self::Ok(OkPayload::parse_for(cmd, payload)?))
            }
            TYPE_ERR => Ok(Self::Err(decode_err_payload(payload)?)),
            TYPE_RX => Ok(Self::Rx(RxPayload::decode(payload)?)),
            TYPE_TX_DONE => Ok(Self::TxDone(TxDonePayload::decode(payload)?)),
            _ => Err(DeviceMessageParseError::UnknownType),
        }
    }
}

// ── Error conversions ───────────────────────────────────────────────

impl From<ModulationParseError> for DeviceMessageParseError {
    fn from(e: ModulationParseError) -> Self {
        match e {
            ModulationParseError::WrongLength { .. } | ModulationParseError::TooShort => {
                Self::WrongLength
            }
            ModulationParseError::InvalidField => Self::InvalidField,
            ModulationParseError::UnknownModulation => Self::UnknownContext,
        }
    }
}

impl From<InfoParseError> for DeviceMessageParseError {
    fn from(e: InfoParseError) -> Self {
        match e {
            InfoParseError::TooShort | InfoParseError::BufferTooSmall => Self::WrongLength,
            InfoParseError::InvalidField => Self::InvalidField,
        }
    }
}

#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode};

    fn sample_lora() -> LoRaConfig {
        LoRaConfig {
            freq_hz: 868_100_000,
            sf: 7,
            bw: LoRaBandwidth::Khz125,
            cr: LoRaCodingRate::Cr4_5,
            preamble_len: 8,
            sync_word: 0x1424,
            tx_power_dbm: 14,
            header_mode: LoRaHeaderMode::Explicit,
            payload_crc: true,
            iq_invert: false,
        }
    }

    #[test]
    fn type_ids_match_spec() {
        assert_eq!(TYPE_OK, 0x80);
        assert_eq!(TYPE_ERR, 0x81);
        assert_eq!(TYPE_RX, 0xC0);
        assert_eq!(TYPE_TX_DONE, 0xC1);
    }

    #[test]
    fn ok_empty_roundtrip_for_ping() {
        let ok = OkPayload::Empty;
        let mut buf = [0u8; 4];
        let n = ok.encode(&mut buf).unwrap();
        assert_eq!(n, 0);
        assert_eq!(
            OkPayload::parse_for(commands::TYPE_PING, &buf[..n]).unwrap(),
            ok
        );
    }

    #[test]
    fn ok_empty_rejects_nonempty_for_tx() {
        assert!(matches!(
            OkPayload::parse_for(commands::TYPE_TX, &[0]),
            Err(DeviceMessageParseError::WrongLength)
        ));
    }

    #[test]
    fn ok_set_config_roundtrip() {
        let r = SetConfigResult {
            result: SetConfigResultCode::Applied,
            owner: Owner::Mine,
            current: Modulation::LoRa(sample_lora()),
        };
        let mut buf = [0u8; 64];
        let n = r.encode(&mut buf).unwrap();
        // 2 (result + owner) + 1 (mod id) + 15 (LoRa).
        assert_eq!(n, 2 + 1 + 15);
        assert_eq!(SetConfigResult::decode(&buf[..n]).unwrap(), r);
    }

    #[test]
    fn ok_set_config_spec_bytes_c23() {
        // PROTOCOL.md §C.2.3 — OK in reply to SET_CONFIG (tag=0x0003).
        // Payload is: result=0, owner=1, mod=0x01, then 15-byte LoRa params.
        let r = SetConfigResult {
            result: SetConfigResultCode::Applied,
            owner: Owner::Mine,
            current: Modulation::LoRa(sample_lora()),
        };
        let mut buf = [0u8; 64];
        let n = r.encode(&mut buf).unwrap();
        let expected: [u8; 18] = [
            0x00, // result APPLIED
            0x01, // owner MINE
            0x01, // modulation_id LoRa
            0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x00, 0x08, 0x00, 0x24, 0x14, 0x0E, 0x00, 0x01,
            0x00,
        ];
        assert_eq!(&buf[..n], &expected);
    }

    #[test]
    fn err_payload_roundtrip() {
        let mut buf = [0u8; 2];
        let n = encode_err_payload(ErrorCode::ENotConfigured, &mut buf).unwrap();
        assert_eq!(n, 2);
        assert_eq!(&buf, &[0x03, 0x00]);
        let decoded = decode_err_payload(&buf).unwrap();
        assert_eq!(decoded, ErrorCode::ENotConfigured);
    }

    #[test]
    fn err_payload_preserves_unknown_codes() {
        let mut buf = [0u8; 2];
        encode_err_payload(ErrorCode::Unknown(0xABCD), &mut buf).unwrap();
        let decoded = decode_err_payload(&buf).unwrap();
        assert_eq!(decoded.as_u16(), 0xABCD);
    }

    #[test]
    fn rx_payload_roundtrip_with_data() {
        let mut data = HVec::new();
        data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
        let rx = RxPayload {
            rssi_tenths_dbm: -735,
            snr_tenths_db: 95,
            freq_err_hz: -125,
            timestamp_us: 42_000_000,
            crc_valid: true,
            packets_dropped: 0,
            origin: RxOrigin::Ota,
            data,
        };
        let mut buf = [0u8; 64];
        let n = rx.encode(&mut buf).unwrap();
        assert_eq!(n, 20 + 4);
        let decoded = RxPayload::decode(&buf[..n]).unwrap();
        assert_eq!(decoded, rx);
    }

    #[test]
    fn rx_payload_spec_bytes_c26() {
        // PROTOCOL.md §C.2.6 — the RX event in the continuous-RX loop.
        let mut data = HVec::new();
        data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
        let rx = RxPayload {
            rssi_tenths_dbm: -735,
            snr_tenths_db: 95,
            freq_err_hz: -125,
            timestamp_us: 42_000_000,
            crc_valid: true,
            packets_dropped: 0,
            origin: RxOrigin::Ota,
            data,
        };
        let mut buf = [0u8; 64];
        let n = rx.encode(&mut buf).unwrap();
        let expected: [u8; 24] = [
            0x21, 0xFD, // rssi -735 = 0xFD21 LE
            0x5F, 0x00, // snr 95 = 0x005F LE
            0x83, 0xFF, 0xFF, 0xFF, // freq_err -125 LE i32
            0x80, 0xDE, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, // timestamp 42_000_000 LE u64
            0x01, // crc_valid
            0x00, 0x00, // packets_dropped
            0x00, // origin OTA
            0x01, 0x02, 0x03, 0x04,
        ];
        assert_eq!(&buf[..n], &expected);
    }

    #[test]
    fn rx_payload_rejects_invalid_crc_byte() {
        let mut buf = [0u8; 20];
        buf[16] = 2; // crc_valid must be 0 or 1
        assert!(RxPayload::decode(&buf).is_err());
    }

    #[test]
    fn tx_done_roundtrip() {
        let td = TxDonePayload {
            result: TxResult::Transmitted,
            airtime_us: 30_976,
        };
        let mut buf = [0u8; 8];
        let n = td.encode(&mut buf).unwrap();
        assert_eq!(n, 5);
        assert_eq!(TxDonePayload::decode(&buf[..n]).unwrap(), td);
    }

    #[test]
    fn tx_done_spec_bytes_c24() {
        // PROTOCOL.md §C.2.4 — TX_DONE for "Hello" at SF7 BW125.
        let td = TxDonePayload {
            result: TxResult::Transmitted,
            airtime_us: 30_976,
        };
        let mut buf = [0u8; 5];
        td.encode(&mut buf).unwrap();
        assert_eq!(&buf, &[0x00, 0x00, 0x79, 0x00, 0x00]);
    }

    #[test]
    fn device_message_parse_requires_ok_context() {
        let mut buf = [0u8; 4];
        let n = OkPayload::Empty.encode(&mut buf).unwrap();
        assert!(matches!(
            DeviceMessage::parse(TYPE_OK, &buf[..n], None),
            Err(DeviceMessageParseError::MissingContext)
        ));
        assert!(matches!(
            DeviceMessage::parse(TYPE_OK, &buf[..n], Some(commands::TYPE_PING)).unwrap(),
            DeviceMessage::Ok(OkPayload::Empty)
        ));
    }

    #[test]
    fn device_message_unknown_type_rejects() {
        assert!(matches!(
            DeviceMessage::parse(0x55, &[], None),
            Err(DeviceMessageParseError::UnknownType)
        ));
    }
}
