// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright by contributors to this project.
// SPDX-License-Identifier: (Apache-2.0 OR MIT)

use core::ops::Deref;

use crate::{client::MlsError, tree_kem::node::LeafIndex, KeyPackage, KeyPackageRef};

use super::{Commit, FramedContentAuthData, GroupInfo, MembershipTag, Welcome};

use crate::group::proposal::{Proposal, ProposalOrRef};

#[cfg(feature = "by_ref_proposal")]
use crate::mls_rules::ProposalRef;

use alloc::vec::Vec;
use core::fmt::{self, Debug};
use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
use mls_rs_core::{
    crypto::{CipherSuite, CipherSuiteProvider},
    protocol_version::ProtocolVersion,
};
use zeroize::ZeroizeOnDrop;

#[cfg(feature = "private_message")]
use alloc::boxed::Box;

#[cfg(feature = "custom_proposal")]
use crate::group::proposal::CustomProposal;

#[derive(Copy, Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum ContentType {
    #[cfg(feature = "private_message")]
    Application = 1u8,
    #[cfg(feature = "by_ref_proposal")]
    Proposal = 2u8,
    Commit = 3u8,
}

impl From<&Content> for ContentType {
    fn from(content: &Content) -> Self {
        match content {
            #[cfg(feature = "private_message")]
            Content::Application(_) => ContentType::Application,
            #[cfg(feature = "by_ref_proposal")]
            Content::Proposal(_) => ContentType::Proposal,
            Content::Commit(_) => ContentType::Commit,
        }
    }
}

#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
#[non_exhaustive]
/// Description of a [`MlsMessage`] sender
pub enum Sender {
    /// Current group member index.
    Member(u32) = 1u8,
    /// An external entity sending a proposal proposal identified by an index
    /// in the current
    /// [`ExternalSendersExt`](crate::extension::ExternalSendersExt) stored in
    /// group context extensions.
    #[cfg(feature = "by_ref_proposal")]
    External(u32) = 2u8,
    /// A new member proposing their own addition to the group.
    #[cfg(feature = "by_ref_proposal")]
    NewMemberProposal = 3u8,
    /// A member sending an external commit.
    NewMemberCommit = 4u8,
}

impl From<LeafIndex> for Sender {
    fn from(leaf_index: LeafIndex) -> Self {
        Sender::Member(*leaf_index)
    }
}

impl From<u32> for Sender {
    fn from(leaf_index: u32) -> Self {
        Sender::Member(leaf_index)
    }
}

#[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode, ZeroizeOnDrop)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ApplicationData(
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
    Vec<u8>,
);

impl Debug for ApplicationData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        mls_rs_core::debug::pretty_bytes(&self.0)
            .named("ApplicationData")
            .fmt(f)
    }
}

impl From<Vec<u8>> for ApplicationData {
    fn from(data: Vec<u8>) -> Self {
        Self(data)
    }
}

impl Deref for ApplicationData {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ApplicationData {
    /// Underlying message content.
    pub fn as_bytes(&self) -> &[u8] {
        &self.0
    }
}

#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub(crate) enum Content {
    #[cfg(feature = "private_message")]
    Application(ApplicationData) = 1u8,
    #[cfg(feature = "by_ref_proposal")]
    Proposal(alloc::boxed::Box<Proposal>) = 2u8,
    Commit(alloc::boxed::Box<Commit>) = 3u8,
}

impl Content {
    pub fn content_type(&self) -> ContentType {
        self.into()
    }
}

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub(crate) struct PublicMessage {
    pub content: FramedContent,
    pub auth: FramedContentAuthData,
    pub membership_tag: Option<MembershipTag>,
}

impl MlsSize for PublicMessage {
    fn mls_encoded_len(&self) -> usize {
        self.content.mls_encoded_len()
            + self.auth.mls_encoded_len()
            + self
                .membership_tag
                .as_ref()
                .map_or(0, |tag| tag.mls_encoded_len())
    }
}

impl MlsEncode for PublicMessage {
    fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
        self.content.mls_encode(writer)?;
        self.auth.mls_encode(writer)?;

        self.membership_tag
            .as_ref()
            .map_or(Ok(()), |tag| tag.mls_encode(writer))
    }
}

impl MlsDecode for PublicMessage {
    fn mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error> {
        let content = FramedContent::mls_decode(reader)?;
        let auth = FramedContentAuthData::mls_decode(reader, content.content_type())?;

        let membership_tag = match content.sender {
            Sender::Member(_) => Some(MembershipTag::mls_decode(reader)?),
            _ => None,
        };

        Ok(Self {
            content,
            auth,
            membership_tag,
        })
    }
}

#[cfg(feature = "private_message")]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct PrivateMessageContent {
    pub content: Content,
    pub auth: FramedContentAuthData,
}

#[cfg(feature = "private_message")]
impl MlsSize for PrivateMessageContent {
    fn mls_encoded_len(&self) -> usize {
        let content_len_without_type = match &self.content {
            Content::Application(c) => c.mls_encoded_len(),
            #[cfg(feature = "by_ref_proposal")]
            Content::Proposal(c) => c.mls_encoded_len(),
            Content::Commit(c) => c.mls_encoded_len(),
        };

        content_len_without_type + self.auth.mls_encoded_len()
    }
}

#[cfg(feature = "private_message")]
impl MlsEncode for PrivateMessageContent {
    fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
        match &self.content {
            Content::Application(c) => c.mls_encode(writer),
            #[cfg(feature = "by_ref_proposal")]
            Content::Proposal(c) => c.mls_encode(writer),
            Content::Commit(c) => c.mls_encode(writer),
        }?;

        self.auth.mls_encode(writer)?;

        Ok(())
    }
}

#[cfg(feature = "private_message")]
impl PrivateMessageContent {
    pub(crate) fn mls_decode(
        reader: &mut &[u8],
        content_type: ContentType,
    ) -> Result<Self, mls_rs_codec::Error> {
        let content = match content_type {
            ContentType::Application => Content::Application(ApplicationData::mls_decode(reader)?),
            #[cfg(feature = "by_ref_proposal")]
            ContentType::Proposal => Content::Proposal(Box::new(Proposal::mls_decode(reader)?)),
            ContentType::Commit => {
                Content::Commit(alloc::boxed::Box::new(Commit::mls_decode(reader)?))
            }
        };

        let auth = FramedContentAuthData::mls_decode(reader, content.content_type())?;

        if reader.iter().any(|&i| i != 0u8) {
            // #[cfg(feature = "std")]
            // return Err(mls_rs_codec::Error::Custom(
            //    "non-zero padding bytes discovered".to_string(),
            // ));

            // #[cfg(not(feature = "std"))]
            return Err(mls_rs_codec::Error::Custom(5));
        }

        Ok(Self { content, auth })
    }
}

#[cfg(feature = "private_message")]
#[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
pub struct PrivateContentAAD {
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub group_id: Vec<u8>,
    pub epoch: u64,
    pub content_type: ContentType,
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub authenticated_data: Vec<u8>,
}

#[cfg(feature = "private_message")]
impl Debug for PrivateContentAAD {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PrivateContentAAD")
            .field(
                "group_id",
                &mls_rs_core::debug::pretty_group_id(&self.group_id),
            )
            .field("epoch", &self.epoch)
            .field("content_type", &self.content_type)
            .field(
                "authenticated_data",
                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
            )
            .finish()
    }
}

#[cfg(feature = "private_message")]
#[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct PrivateMessage {
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub group_id: Vec<u8>,
    pub epoch: u64,
    pub content_type: ContentType,
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub authenticated_data: Vec<u8>,
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub encrypted_sender_data: Vec<u8>,
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub ciphertext: Vec<u8>,
}

#[cfg(feature = "private_message")]
impl Debug for PrivateMessage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PrivateMessage")
            .field(
                "group_id",
                &mls_rs_core::debug::pretty_group_id(&self.group_id),
            )
            .field("epoch", &self.epoch)
            .field("content_type", &self.content_type)
            .field(
                "authenticated_data",
                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
            )
            .field(
                "encrypted_sender_data",
                &mls_rs_core::debug::pretty_bytes(&self.encrypted_sender_data),
            )
            .field(
                "ciphertext",
                &mls_rs_core::debug::pretty_bytes(&self.ciphertext),
            )
            .finish()
    }
}

#[cfg(feature = "private_message")]
impl From<&PrivateMessage> for PrivateContentAAD {
    fn from(ciphertext: &PrivateMessage) -> Self {
        Self {
            group_id: ciphertext.group_id.clone(),
            epoch: ciphertext.epoch,
            content_type: ciphertext.content_type,
            authenticated_data: ciphertext.authenticated_data.clone(),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum MlsMessageDescription<'a> {
    Welcome {
        key_package_refs: Vec<&'a KeyPackageRef>,
        cipher_suite: CipherSuite,
    },
    PrivateProtocolMessage {
        group_id: &'a [u8],
        epoch_id: u64,
        content_type: ContentType, // commit, proposal, or application
    },
    PublicProtocolMessage {
        group_id: &'a [u8],
        epoch_id: u64,
        content_type: ContentType,
        sender: Sender,
        authenticated_data: &'a [u8],
    },
    GroupInfo,
    KeyPackage,
}

impl MlsMessage {
    pub fn description(&self) -> MlsMessageDescription<'_> {
        match &self.payload {
            MlsMessagePayload::Welcome(w) => MlsMessageDescription::Welcome {
                key_package_refs: w.secrets.iter().map(|s| &s.new_member).collect(),
                cipher_suite: w.cipher_suite,
            },
            MlsMessagePayload::Plain(p) => MlsMessageDescription::PublicProtocolMessage {
                group_id: &p.content.group_id,
                epoch_id: p.content.epoch,
                content_type: p.content.content_type(),
                sender: p.content.sender,
                authenticated_data: &p.content.authenticated_data,
            },
            #[cfg(feature = "private_message")]
            MlsMessagePayload::Cipher(c) => MlsMessageDescription::PrivateProtocolMessage {
                group_id: &c.group_id,
                epoch_id: c.epoch,
                content_type: c.content_type,
            },
            MlsMessagePayload::GroupInfo(_) => MlsMessageDescription::GroupInfo,
            MlsMessagePayload::KeyPackage(_) => MlsMessageDescription::KeyPackage,
        }
    }
}

#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(
    all(feature = "ffi", not(test)),
    ::safer_ffi_gen::ffi_type(clone, opaque)
)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
/// A MLS protocol message for sending data over the wire.
pub struct MlsMessage {
    pub(crate) version: ProtocolVersion,
    pub(crate) payload: MlsMessagePayload,
}

#[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)]
#[allow(dead_code)]
impl MlsMessage {
    pub(crate) fn new(version: ProtocolVersion, payload: MlsMessagePayload) -> MlsMessage {
        Self { version, payload }
    }

    #[inline(always)]
    pub(crate) fn into_plaintext(self) -> Option<PublicMessage> {
        match self.payload {
            MlsMessagePayload::Plain(plaintext) => Some(plaintext),
            _ => None,
        }
    }

    #[cfg(feature = "private_message")]
    #[inline(always)]
    pub(crate) fn into_ciphertext(self) -> Option<PrivateMessage> {
        match self.payload {
            MlsMessagePayload::Cipher(ciphertext) => Some(ciphertext),
            _ => None,
        }
    }

    #[inline(always)]
    pub(crate) fn into_welcome(self) -> Option<Welcome> {
        match self.payload {
            MlsMessagePayload::Welcome(welcome) => Some(welcome),
            _ => None,
        }
    }

    #[inline(always)]
    pub fn into_group_info(self) -> Option<GroupInfo> {
        match self.payload {
            MlsMessagePayload::GroupInfo(info) => Some(info),
            _ => None,
        }
    }

    #[inline(always)]
    pub fn as_group_info(&self) -> Option<&GroupInfo> {
        match &self.payload {
            MlsMessagePayload::GroupInfo(info) => Some(info),
            _ => None,
        }
    }

    #[inline(always)]
    pub fn into_key_package(self) -> Option<KeyPackage> {
        match self.payload {
            MlsMessagePayload::KeyPackage(kp) => Some(kp),
            _ => None,
        }
    }

    pub fn as_key_package(&self) -> Option<&KeyPackage> {
        match &self.payload {
            MlsMessagePayload::KeyPackage(kp) => Some(kp),
            _ => None,
        }
    }

    /// The wire format value describing the contents of this message.
    pub fn wire_format(&self) -> WireFormat {
        match self.payload {
            MlsMessagePayload::Plain(_) => WireFormat::PublicMessage,
            #[cfg(feature = "private_message")]
            MlsMessagePayload::Cipher(_) => WireFormat::PrivateMessage,
            MlsMessagePayload::Welcome(_) => WireFormat::Welcome,
            MlsMessagePayload::GroupInfo(_) => WireFormat::GroupInfo,
            MlsMessagePayload::KeyPackage(_) => WireFormat::KeyPackage,
        }
    }

    /// The epoch that this message belongs to.
    ///
    /// Returns `None` if the message is [`WireFormat::KeyPackage`]
    /// or [`WireFormat::Welcome`]
    #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen_ignore)]
    pub fn epoch(&self) -> Option<u64> {
        match &self.payload {
            MlsMessagePayload::Plain(p) => Some(p.content.epoch),
            #[cfg(feature = "private_message")]
            MlsMessagePayload::Cipher(c) => Some(c.epoch),
            MlsMessagePayload::GroupInfo(gi) => Some(gi.group_context.epoch),
            _ => None,
        }
    }

    #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen_ignore)]
    pub fn cipher_suite(&self) -> Option<CipherSuite> {
        match &self.payload {
            MlsMessagePayload::GroupInfo(i) => Some(i.group_context.cipher_suite),
            MlsMessagePayload::Welcome(w) => Some(w.cipher_suite),
            MlsMessagePayload::KeyPackage(k) => Some(k.cipher_suite),
            _ => None,
        }
    }

    pub fn group_id(&self) -> Option<&[u8]> {
        match &self.payload {
            MlsMessagePayload::Plain(p) => Some(&p.content.group_id),
            #[cfg(feature = "private_message")]
            MlsMessagePayload::Cipher(p) => Some(&p.group_id),
            MlsMessagePayload::GroupInfo(p) => Some(&p.group_context.group_id),
            MlsMessagePayload::KeyPackage(_) | MlsMessagePayload::Welcome(_) => None,
        }
    }

    /// Deserialize a message from transport.
    #[inline(never)]
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
        Self::mls_decode(&mut &*bytes).map_err(Into::into)
    }

    /// Serialize a message for transport.
    pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
        self.mls_encode_to_vec().map_err(Into::into)
    }

    /// If this is a plaintext commit message, return all custom proposals committed by value.
    /// If this is not a plaintext or not a commit, this returns an empty list.
    #[cfg(feature = "custom_proposal")]
    pub fn custom_proposals_by_value(&self) -> Vec<&CustomProposal> {
        match &self.payload {
            MlsMessagePayload::Plain(plaintext) => match &plaintext.content.content {
                Content::Commit(commit) => Self::find_custom_proposals(commit),
                _ => Vec::new(),
            },
            _ => Vec::new(),
        }
    }

    /// If this is a plaintext commit message, return all proposals committed by value.
    /// If this is not a plaintext or not a commit, this returns an empty list.
    /// **Note**: This method is not available in FFI bindings due to Proposal type constraints.
    #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen_ignore)]
    #[allow(unreachable_patterns)]
    pub fn proposals_by_value(&self) -> Vec<&Proposal> {
        match &self.payload {
            MlsMessagePayload::Plain(plaintext) => match &plaintext.content.content {
                Content::Commit(commit) => Self::find_all_proposals(commit),
                _ => Vec::new(),
            },
            _ => Vec::new(),
        }
    }

    /// If this is a welcome message, return key package references of all members who can
    /// join using this message.
    pub fn welcome_key_package_references(&self) -> Vec<&KeyPackageRef> {
        let MlsMessagePayload::Welcome(welcome) = &self.payload else {
            return Vec::new();
        };

        welcome.secrets.iter().map(|s| &s.new_member).collect()
    }

    /// If this is a key package, return its key package reference.
    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
    pub async fn key_package_reference<C: CipherSuiteProvider>(
        &self,
        cipher_suite: &C,
    ) -> Result<Option<KeyPackageRef>, MlsError> {
        let MlsMessagePayload::KeyPackage(kp) = &self.payload else {
            return Ok(None);
        };

        kp.to_reference(cipher_suite).await.map(Some)
    }

    /// If this is a plaintext proposal, return the proposal reference that can be matched e.g. with
    /// [`NewEpoch::unused_proposals`](super::NewEpoch::unused_proposals).
    #[cfg(feature = "by_ref_proposal")]
    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
    pub async fn into_proposal_reference<C: CipherSuiteProvider>(
        self,
        cipher_suite: &C,
    ) -> Result<Option<Vec<u8>>, MlsError> {
        let MlsMessagePayload::Plain(public_message) = self.payload else {
            return Ok(None);
        };

        ProposalRef::from_content(cipher_suite, &public_message.into())
            .await
            .map(|r| Some(r.to_vec()))
    }
}

impl MlsMessage {
    #[cfg(feature = "custom_proposal")]
    fn find_custom_proposals(commit: &Commit) -> Vec<&CustomProposal> {
        commit
            .proposals
            .iter()
            .filter_map(|p| match p {
                ProposalOrRef::Proposal(p) => match p.as_ref() {
                    crate::group::Proposal::Custom(p) => Some(p),
                    _ => None,
                },
                _ => None,
            })
            .collect()
    }

    #[allow(unreachable_patterns)]
    fn find_all_proposals(commit: &Commit) -> Vec<&Proposal> {
        commit
            .proposals
            .iter()
            .filter_map(|p| match p {
                ProposalOrRef::Proposal(p) => Some(p.as_ref()),
                _ => None,
            })
            .collect()
    }
}

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u16)]
pub(crate) enum MlsMessagePayload {
    Plain(PublicMessage) = 1u16,
    #[cfg(feature = "private_message")]
    Cipher(PrivateMessage) = 2u16,
    Welcome(Welcome) = 3u16,
    GroupInfo(GroupInfo) = 4u16,
    KeyPackage(KeyPackage) = 5u16,
}

impl From<PublicMessage> for MlsMessagePayload {
    fn from(m: PublicMessage) -> Self {
        Self::Plain(m)
    }
}

#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
#[derive(
    Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, MlsSize, MlsEncode, MlsDecode,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u16)]
#[non_exhaustive]
/// Content description of an [`MlsMessage`]
pub enum WireFormat {
    PublicMessage = 1u16,
    PrivateMessage = 2u16,
    Welcome = 3u16,
    GroupInfo = 4u16,
    KeyPackage = 5u16,
}

#[derive(Clone, PartialEq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct FramedContent {
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
    pub group_id: Vec<u8>,
    pub epoch: u64,
    pub sender: Sender,
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
    pub authenticated_data: Vec<u8>,
    pub content: Content,
}

impl Debug for FramedContent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("FramedContent")
            .field(
                "group_id",
                &mls_rs_core::debug::pretty_group_id(&self.group_id),
            )
            .field("epoch", &self.epoch)
            .field("sender", &self.sender)
            .field(
                "authenticated_data",
                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
            )
            .field("content", &self.content)
            .finish()
    }
}

impl FramedContent {
    pub fn content_type(&self) -> ContentType {
        self.content.content_type()
    }
}

#[cfg(test)]
pub(crate) mod test_utils {
    #[cfg(feature = "private_message")]
    use crate::group::test_utils::random_bytes;

    use crate::group::{AuthenticatedContent, MessageSignature};

    use super::*;

    use alloc::boxed::Box;

    pub(crate) fn get_test_auth_content() -> AuthenticatedContent {
        // This is not a valid commit and should not be validated
        let commit = Commit {
            proposals: Default::default(),
            path: None,
        };

        AuthenticatedContent {
            wire_format: WireFormat::PublicMessage,
            content: FramedContent {
                group_id: Vec::new(),
                epoch: 0,
                sender: Sender::Member(1),
                authenticated_data: Vec::new(),
                content: Content::Commit(Box::new(commit)),
            },
            auth: FramedContentAuthData {
                signature: MessageSignature::empty(),
                confirmation_tag: None,
            },
        }
    }

    #[cfg(feature = "private_message")]
    pub(crate) fn get_test_ciphertext_content() -> PrivateMessageContent {
        PrivateMessageContent {
            content: Content::Application(random_bytes(1024).into()),
            auth: FramedContentAuthData {
                signature: MessageSignature::from(random_bytes(128)),
                confirmation_tag: None,
            },
        }
    }

    impl AsRef<[u8]> for ApplicationData {
        fn as_ref(&self) -> &[u8] {
            &self.0
        }
    }
}

#[cfg(feature = "private_message")]
#[cfg(test)]
mod tests {
    use alloc::vec;
    use assert_matches::assert_matches;

    use crate::{
        client::test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
        crypto::test_utils::test_cipher_suite_provider,
        group::{
            framing::test_utils::get_test_ciphertext_content,
            proposal_ref::test_utils::auth_content_from_proposal, test_utils::test_group,
            RemoveProposal,
        },
        key_package::test_utils::test_key_package_message,
    };

    use super::*;

    #[test]
    fn test_mls_ciphertext_content_mls_encoding() {
        let ciphertext_content = get_test_ciphertext_content();

        let mut encoded = ciphertext_content.mls_encode_to_vec().unwrap();
        encoded.extend_from_slice(&[0u8; 128]);

        let decoded =
            PrivateMessageContent::mls_decode(&mut &*encoded, (&ciphertext_content.content).into())
                .unwrap();

        assert_eq!(ciphertext_content, decoded);
    }

    #[test]
    fn test_mls_ciphertext_content_non_zero_padding_error() {
        let ciphertext_content = get_test_ciphertext_content();

        let mut encoded = ciphertext_content.mls_encode_to_vec().unwrap();
        encoded.extend_from_slice(&[1u8; 128]);

        let decoded =
            PrivateMessageContent::mls_decode(&mut &*encoded, (&ciphertext_content.content).into());

        assert_matches!(decoded, Err(mls_rs_codec::Error::Custom(_)));
    }

    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
    async fn proposal_ref() {
        let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);

        let test_auth = auth_content_from_proposal(
            Proposal::Remove(RemoveProposal {
                to_remove: LeafIndex::unchecked(0),
            }),
            Sender::External(0),
        );

        let expected_ref = ProposalRef::from_content(&cs, &test_auth).await.unwrap();

        let test_message = MlsMessage {
            version: TEST_PROTOCOL_VERSION,
            payload: MlsMessagePayload::Plain(PublicMessage {
                content: test_auth.content,
                auth: test_auth.auth,
                membership_tag: Some(cs.mac(&[1, 2, 3], &[1, 2, 3]).await.unwrap().into()),
            }),
        };

        let computed_ref = test_message
            .into_proposal_reference(&cs)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(computed_ref, expected_ref.to_vec());
    }

    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
    async fn message_description() {
        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;

        let message = group.commit(vec![]).await.unwrap();

        let expected = MlsMessageDescription::PublicProtocolMessage {
            group_id: group.group_id(),
            epoch_id: group.context().epoch,
            content_type: ContentType::Commit,
            sender: Sender::Member(0),
            authenticated_data: &[],
        };

        assert_eq!(message.commit_message.description(), expected);

        group.apply_pending_commit().await.unwrap();

        let message = group
            .encrypt_application_message(b"123", vec![])
            .await
            .unwrap();

        let expected = MlsMessageDescription::PrivateProtocolMessage {
            group_id: group.group_id(),
            epoch_id: group.context().epoch,
            content_type: ContentType::Application,
        };

        assert_eq!(message.description(), expected);

        let group_info = group
            .group_info_message_allowing_ext_commit(true)
            .await
            .unwrap();

        assert_eq!(group_info.description(), MlsMessageDescription::GroupInfo);

        let key_package =
            test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "something").await;

        assert_eq!(key_package.description(), MlsMessageDescription::KeyPackage);
    }
}
