//! A Blaze message: a Fire2 frame whose payload is decoded Heat2/TDF.
//!
//! This is the highest level this crate goes. It routes by *numbers*, never by
//! names: mapping `0x0009/0x0007` to `Util::preAuth`, or knowing which fields a
//! login reply must carry, is a per-title concern that belongs in a game
//! adapter. Keeping that out is what lets a second title reuse this layer
//! without inheriting FIFA 17's command tables.

use crate::error::Result;
use crate::fire2::{Frame, Header, MsgType};
use crate::heat2::{self, Struct};

/// A frame plus its decoded body.
#[derive(Debug, Clone, PartialEq)]
pub struct Message {
    pub header: Header,
    pub body: Struct,
}

impl Message {
    pub fn new(
        component: u16,
        command: u16,
        msg_num: u32,
        msg_type: MsgType,
        body: Struct,
    ) -> Message {
        Message {
            header: Header::new(component, command, msg_num, msg_type),
            body,
        }
    }

    /// Decode a frame's payload as TDF.
    pub fn from_frame(frame: &Frame) -> Result<Message> {
        Ok(Message {
            header: frame.header,
            body: heat2::decode(&frame.payload)?,
        })
    }

    /// Re-frame this message.
    ///
    /// Note this is not guaranteed byte-identical to the frame a `Message` was
    /// decoded from: encoding sorts members by packed tag, so a peer that sent
    /// them out of order would see its ordering normalised. For byte-exact
    /// round trips (capture replay, differential tests) keep the [`Frame`] and
    /// its raw payload.
    pub fn to_frame(&self) -> Frame {
        let payload = heat2::encode(&self.body);
        let mut frame = Frame::new(
            self.header.component,
            self.header.command,
            self.header.msg_num,
            self.header.msg_type,
            payload,
        );
        frame.header.user_index = self.header.user_index;
        frame.header.options = self.header.options;
        frame
    }

    pub fn encode(&self) -> Vec<u8> {
        self.to_frame().encode()
    }

    /// Component and command as a pair — the key an adapter dispatches on.
    pub fn route(&self) -> (u16, u16) {
        (self.header.component, self.header.command)
    }
}

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

    #[test]
    fn decodes_a_frame_body_and_reframes_it() {
        let body = Struct::new()
            .with("PID", Value::Int(33068179))
            .with("NAME", Value::String("CAGE".into()));
        let msg = Message::new(0x0001, 0x000A, 7, MsgType::Reply, body.clone());

        let bytes = msg.encode();
        let (frame, used) = Frame::parse(&bytes).unwrap();
        assert_eq!(used, bytes.len());

        let back = Message::from_frame(&frame).unwrap();
        assert_eq!(back.route(), (0x0001, 0x000A));
        assert_eq!(back.header.msg_type, MsgType::Reply);
        assert_eq!(back.body.get("NAME").and_then(Value::as_str), Some("CAGE"));
        assert_eq!(back.encode(), bytes);
    }

    #[test]
    fn an_empty_body_is_a_valid_message() {
        let msg = Message::new(0x0009, 0x0002, 1, MsgType::Reply, Struct::new());
        let (frame, _) = Frame::parse(&msg.encode()).unwrap();
        assert_eq!(frame.header.payload_len, 0);
        assert!(Message::from_frame(&frame).unwrap().body.is_empty());
    }
}
