//! Types for the [`m.room.member`] event.
//!
//! [`m.room.member`]: https://spec.matrix.org/v1.2/client-server-api/#mroommember

use std::collections::BTreeMap;

use js_int::Int;
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};

use crate::{
    events::{
        AnyStrippedStateEvent, EventContent, HasDeserializeFields, RedactContent,
        RedactedEventContent, RedactedStateEventContent, Relations, StateEventContent,
        StateEventType, StateUnsigned, StateUnsignedFromParts, StaticEventContent,
    },
    serde::{CanBeEmpty, Raw, StringEnum},
    OwnedMxcUri, OwnedServerName, OwnedServerSigningKeyId, OwnedTransactionId, OwnedUserId,
    PrivOwnedStr, RoomVersionId,
};

mod change;

use self::change::membership_change;
pub use self::change::{Change, MembershipChange, MembershipDetails};

/// The content of an `m.room.member` event.
///
/// The current membership state of a user in the room.
///
/// Adjusts the membership state for a user in a room. It is preferable to use the membership
/// APIs (`/rooms/<room id>/invite` etc) when performing membership actions rather than
/// adjusting the state directly as there are a restricted set of valid transformations. For
/// example, user A cannot force user B to join a room, and trying to force this state change
/// directly will fail.
///
/// This event may also include an `invite_room_state` key inside the event's unsigned data, but
/// Ruma doesn't currently expose this; see [#998](https://github.com/ruma/ruma/issues/998).
///
/// The user for which a membership applies is represented by the `state_key`. Under some
/// conditions, the `sender` and `state_key` may not match - this may be interpreted as the
/// `sender` affecting the membership state of the `state_key` user.
///
/// The membership for a given user can change over time. Previous membership can be retrieved
/// from the `prev_content` object on an event. If not present, the user's previous membership
/// must be assumed as leave.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(
    type = "m.room.member",
    kind = State,
    state_key_type = OwnedUserId,
    unsigned_type = RoomMemberUnsigned,
    custom_redacted,
)]
pub struct RoomMemberEventContent {
    /// The avatar URL for this user, if any.
    ///
    /// This is added by the homeserver. If you activate the `compat` feature, this field being an
    /// empty string in JSON will result in `None` here during deserialization.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(
        feature = "compat",
        serde(default, deserialize_with = "crate::serde::empty_string_as_none")
    )]
    pub avatar_url: Option<OwnedMxcUri>,

    /// The display name for this user, if any.
    ///
    /// This is added by the homeserver.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub displayname: Option<String>,

    /// Flag indicating whether the room containing this event was created with the intention of
    /// being a direct chat.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_direct: Option<bool>,

    /// The membership state of this user.
    pub membership: MembershipState,

    /// If this member event is the successor to a third party invitation, this field will
    /// contain information about that invitation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub third_party_invite: Option<ThirdPartyInvite>,

    /// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`.
    ///
    /// This uses the unstable prefix in
    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
    #[cfg(feature = "unstable-msc2448")]
    #[serde(
        rename = "xyz.amorgan.blurhash",
        alias = "blurhash",
        skip_serializing_if = "Option::is_none"
    )]
    pub blurhash: Option<String>,

    /// User-supplied text for why their membership has changed.
    ///
    /// For kicks and bans, this is typically the reason for the kick or ban. For other membership
    /// changes, this is a way for the user to communicate their intent without having to send a
    /// message to the room, such as in a case where Bob rejects an invite from Alice about an
    /// upcoming concert, but can't make it that day.
    ///
    /// Clients are not recommended to show this reason to users when receiving an invite due to
    /// the potential for spam and abuse. Hiding the reason behind a button or other component
    /// is recommended.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,

    /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite.
    #[serde(rename = "join_authorised_via_users_server")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub join_authorized_via_users_server: Option<OwnedUserId>,
}

impl RoomMemberEventContent {
    /// Creates a new `RoomMemberEventContent` with the given membership state.
    pub fn new(membership: MembershipState) -> Self {
        Self {
            membership,
            avatar_url: None,
            displayname: None,
            is_direct: None,
            third_party_invite: None,
            #[cfg(feature = "unstable-msc2448")]
            blurhash: None,
            reason: None,
            join_authorized_via_users_server: None,
        }
    }

    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        MembershipDetails {
            avatar_url: self.avatar_url.as_deref(),
            displayname: self.displayname.as_deref(),
            membership: &self.membership,
        }
    }
}

impl RedactContent for RoomMemberEventContent {
    type Redacted = RedactedRoomMemberEventContent;

    fn redact(self, _version: &RoomVersionId) -> RedactedRoomMemberEventContent {
        RedactedRoomMemberEventContent {
            membership: self.membership,
            join_authorized_via_users_server: match _version {
                RoomVersionId::V9 | RoomVersionId::V10 => self.join_authorized_via_users_server,
                _ => None,
            },
        }
    }
}

/// A member event that has been redacted.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct RedactedRoomMemberEventContent {
    /// The membership state of this user.
    pub membership: MembershipState,

    /// An arbitrary user who has the power to issue invites.
    ///
    /// This is redacted in room versions 8 and below. It is used for validating
    /// joins when the join rule is restricted.
    #[serde(rename = "join_authorised_via_users_server")]
    pub join_authorized_via_users_server: Option<OwnedUserId>,
}

impl RedactedRoomMemberEventContent {
    /// Create a `RedactedRoomMemberEventContent` with the given membership.
    pub fn new(membership: MembershipState) -> Self {
        Self { membership, join_authorized_via_users_server: None }
    }

    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
    }
}

impl EventContent for RedactedRoomMemberEventContent {
    type EventType = StateEventType;

    fn event_type(&self) -> StateEventType {
        StateEventType::RoomMember
    }

    fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self> {
        if event_type != "m.room.member" {
            return Err(::serde::de::Error::custom(format!(
                "expected event type `m.room.member`, found `{}`",
                event_type
            )));
        }

        serde_json::from_str(content.get())
    }
}

impl StateEventContent for RedactedRoomMemberEventContent {
    type StateKey = OwnedUserId;
    // FIXME: Not actually used
    type Unsigned = StateUnsigned<Self>;
}

impl RedactedStateEventContent for RedactedRoomMemberEventContent {}

// Since this redacted event has fields we leave the default `empty` method
// that will error if called.
impl RedactedEventContent for RedactedRoomMemberEventContent {
    fn has_serialize_fields(&self) -> bool {
        true
    }

    fn has_deserialize_fields() -> HasDeserializeFields {
        HasDeserializeFields::Optional
    }
}

impl RoomMemberEvent {
    /// Obtain the membership state, regardless of whether this event is redacted.
    pub fn membership(&self) -> &MembershipState {
        match self {
            Self::Original(ev) => &ev.content.membership,
            Self::Redacted(ev) => &ev.content.membership,
        }
    }
}

impl SyncRoomMemberEvent {
    /// Obtain the membership state, regardless of whether this event is redacted.
    pub fn membership(&self) -> &MembershipState {
        match self {
            Self::Original(ev) => &ev.content.membership,
            Self::Redacted(ev) => &ev.content.membership,
        }
    }
}

/// The membership state of a user.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]
#[ruma_enum(rename_all = "lowercase")]
#[non_exhaustive]
pub enum MembershipState {
    /// The user is banned.
    Ban,

    /// The user has been invited.
    Invite,

    /// The user has joined.
    Join,

    /// The user has requested to join.
    Knock,

    /// The user has left.
    Leave,

    #[doc(hidden)]
    _Custom(PrivOwnedStr),
}

/// Information about a third party invitation.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ThirdPartyInvite {
    /// A name which can be displayed to represent the user instead of their third party
    /// identifier.
    pub display_name: String,

    /// A block of content which has been signed, which servers can use to verify the event.
    ///
    /// Clients should ignore this.
    pub signed: SignedContent,
}

impl ThirdPartyInvite {
    /// Creates a new `ThirdPartyInvite` with the given display name and signed content.
    pub fn new(display_name: String, signed: SignedContent) -> Self {
        Self { display_name, signed }
    }
}

/// A block of content which has been signed, which servers can use to verify a third party
/// invitation.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SignedContent {
    /// The invited Matrix user ID.
    ///
    /// Must be equal to the user_id property of the event.
    pub mxid: OwnedUserId,

    /// A single signature from the verifying server, in the format specified by the Signing Events
    /// section of the server-server API.
    pub signatures: BTreeMap<OwnedServerName, BTreeMap<OwnedServerSigningKeyId, String>>,

    /// The token property of the containing `third_party_invite` object.
    pub token: String,
}

impl SignedContent {
    /// Creates a new `SignedContent` with the given mxid, signature and token.
    pub fn new(
        signatures: BTreeMap<OwnedServerName, BTreeMap<OwnedServerSigningKeyId, String>>,
        mxid: OwnedUserId,
        token: String,
    ) -> Self {
        Self { mxid, signatures, token }
    }
}

impl OriginalRoomMemberEvent {
    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        self.content.details()
    }

    /// Get a reference to the `prev_content` in unsigned, if it exists.
    ///
    /// Shorthand for `event.unsigned.prev_content.as_ref()`
    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
        self.unsigned.prev_content.as_ref()
    }

    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
        self.prev_content().map(|c| c.details())
    }

    /// Helper function for membership change.
    ///
    /// Check [the specification][spec] for details.
    ///
    /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember
    pub fn membership_change(&self) -> MembershipChange<'_> {
        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
    }
}

impl RedactedRoomMemberEvent {
    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        self.content.details()
    }

    /// Helper function for membership change.
    ///
    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
    /// event).
    ///
    /// Check [the specification][spec] for details.
    ///
    /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember
    pub fn membership_change<'a>(
        &'a self,
        prev_details: Option<MembershipDetails<'a>>,
    ) -> MembershipChange<'a> {
        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
    }
}

impl OriginalSyncRoomMemberEvent {
    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        self.content.details()
    }

    /// Get a reference to the `prev_content` in unsigned, if it exists.
    ///
    /// Shorthand for `event.unsigned.prev_content.as_ref()`
    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
        self.unsigned.prev_content.as_ref()
    }

    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
        self.prev_content().map(|c| c.details())
    }

    /// Helper function for membership change.
    ///
    /// Check [the specification][spec] for details.
    ///
    /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember
    pub fn membership_change(&self) -> MembershipChange<'_> {
        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
    }
}

impl RedactedSyncRoomMemberEvent {
    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        self.content.details()
    }

    /// Helper function for membership change.
    ///
    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
    /// event).
    ///
    /// Check [the specification][spec] for details.
    ///
    /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember
    pub fn membership_change<'a>(
        &'a self,
        prev_details: Option<MembershipDetails<'a>>,
    ) -> MembershipChange<'a> {
        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
    }
}

impl StrippedRoomMemberEvent {
    /// Obtain the details about this event that are required to calculate a membership change.
    ///
    /// This is required when you want to calculate the change a redacted `m.room.member` event
    /// made.
    pub fn details(&self) -> MembershipDetails<'_> {
        self.content.details()
    }

    /// Helper function for membership change.
    ///
    /// Since stripped events don't have `unsigned.prev_content`, you have to pass the `.details()`
    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
    /// event).
    ///
    /// Check [the specification][spec] for details.
    ///
    /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember
    pub fn membership_change<'a>(
        &'a self,
        prev_details: Option<MembershipDetails<'a>>,
    ) -> MembershipChange<'a> {
        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
    }
}

/// Extra information about a message event that is not incorporated into the event's hash.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct RoomMemberUnsigned {
    /// The time in milliseconds that has elapsed since the event was sent.
    ///
    /// This field is generated by the local homeserver, and may be incorrect if the local time on
    /// at least one of the two servers is out of sync, which can cause the age to either be
    /// negative or greater than it actually is.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub age: Option<Int>,

    /// The client-supplied transaction ID, if the client being given the event is the same one
    /// which sent it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transaction_id: Option<OwnedTransactionId>,

    /// Optional previous content of the event.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prev_content: Option<RoomMemberEventContent>,

    /// State events to assist the receiver in identifying the room.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,

    /// [Bundled aggregations] of related child events.
    ///
    /// [Bundled aggregations]: https://spec.matrix.org/v1.3/client-server-api/#aggregations
    #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")]
    pub relations: Option<Relations>,
}

impl RoomMemberUnsigned {
    /// Create a new `Unsigned` with fields set to `None`.
    pub fn new() -> Self {
        Self::default()
    }
}

impl CanBeEmpty for RoomMemberUnsigned {
    /// Whether this unsigned data is empty (all fields are `None`).
    ///
    /// This method is used to determine whether to skip serializing the `unsigned` field in room
    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
    /// could still have been present but contained none of the known fields.
    fn is_empty(&self) -> bool {
        self.age.is_none()
            && self.transaction_id.is_none()
            && self.prev_content.is_none()
            && self.invite_room_state.is_empty()
            && self.relations.is_none()
    }
}

impl StateUnsignedFromParts for RoomMemberUnsigned {
    fn _from_parts(event_type: &str, object: &RawJsonValue) -> serde_json::Result<Self> {
        const EVENT_TYPE: &str = <RoomMemberEventContent as StaticEventContent>::TYPE;

        if event_type != EVENT_TYPE {
            return Err(serde::de::Error::custom(format!(
                "expected event type of `{EVENT_TYPE}`, found `{event_type}`",
            )));
        }

        from_json_str(object.get())
    }
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use js_int::uint;
    use maplit::btreemap;
    use serde_json::{from_value as from_json_value, json};

    use super::{MembershipState, RoomMemberEventContent};
    use crate::{
        events::OriginalStateEvent, mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_id,
        user_id, MilliSecondsSinceUnixEpoch,
    };

    #[test]
    fn serde_with_no_prev_content() {
        let json = json!({
            "type": "m.room.member",
            "content": {
                "membership": "join"
            },
            "event_id": "$h29iv0s8:example.com",
            "origin_server_ts": 1,
            "room_id": "!n8f893n9:example.com",
            "sender": "@carl:example.com",
            "state_key": "@carl:example.com"
        });

        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
        assert_eq!(ev.room_id, "!n8f893n9:example.com");
        assert_eq!(ev.sender, "@carl:example.com");
        assert_eq!(ev.state_key, "@carl:example.com");
        assert!(ev.unsigned.is_empty());

        assert_eq!(ev.content.avatar_url, None);
        assert_eq!(ev.content.displayname, None);
        assert_eq!(ev.content.is_direct, None);
        assert_eq!(ev.content.membership, MembershipState::Join);
        assert_matches!(ev.content.third_party_invite, None);
    }

    #[test]
    fn serde_with_prev_content() {
        let json = json!({
            "type": "m.room.member",
            "content": {
                "membership": "join"
            },
            "event_id": "$h29iv0s8:example.com",
            "origin_server_ts": 1,
            "room_id": "!n8f893n9:example.com",
            "sender": "@carl:example.com",
            "state_key": "@carl:example.com",
            "unsigned": {
                "prev_content": {
                    "membership": "join"
                },
            },
        });

        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
        assert_eq!(ev.room_id, "!n8f893n9:example.com");
        assert_eq!(ev.sender, "@carl:example.com");
        assert_eq!(ev.state_key, "@carl:example.com");

        assert_eq!(ev.content.avatar_url, None);
        assert_eq!(ev.content.displayname, None);
        assert_eq!(ev.content.is_direct, None);
        assert_eq!(ev.content.membership, MembershipState::Join);
        assert_matches!(ev.content.third_party_invite, None);

        let prev_content = ev.unsigned.prev_content.unwrap();
        assert_eq!(prev_content.avatar_url, None);
        assert_eq!(prev_content.displayname, None);
        assert_eq!(prev_content.is_direct, None);
        assert_eq!(prev_content.membership, MembershipState::Join);
        assert_matches!(prev_content.third_party_invite, None);
    }

    #[test]
    fn serde_with_content_full() {
        let json = json!({
            "type": "m.room.member",
            "content": {
                "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
                "displayname": "Alice Margatroid",
                "is_direct": true,
                "membership": "invite",
                "third_party_invite": {
                    "display_name": "alice",
                    "signed": {
                        "mxid": "@alice:example.org",
                        "signatures": {
                            "magic.forest": {
                                "ed25519:3": "foobar"
                            }
                        },
                        "token": "abc123"
                    }
                }
            },
            "event_id": "$143273582443PhrSn:example.org",
            "origin_server_ts": 233,
            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
            "sender": "@alice:example.org",
            "state_key": "@alice:example.org"
        });

        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
        assert_eq!(ev.sender, "@alice:example.org");
        assert_eq!(ev.state_key, "@alice:example.org");
        assert!(ev.unsigned.is_empty());

        assert_eq!(
            ev.content.avatar_url.as_deref(),
            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
        );
        assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
        assert_eq!(ev.content.is_direct, Some(true));
        assert_eq!(ev.content.membership, MembershipState::Invite);

        let third_party_invite = ev.content.third_party_invite.unwrap();
        assert_eq!(third_party_invite.display_name, "alice");
        assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
        assert_eq!(
            third_party_invite.signed.signatures,
            btreemap! {
                server_name!("magic.forest").to_owned() => btreemap! {
                    server_signing_key_id!("ed25519:3").to_owned() => "foobar".to_owned()
                }
            }
        );
        assert_eq!(third_party_invite.signed.token, "abc123");
    }

    #[test]
    fn serde_with_prev_content_full() {
        let json = json!({
            "type": "m.room.member",
            "content": {
                "membership": "join",
            },
            "event_id": "$143273582443PhrSn:example.org",
            "origin_server_ts": 233,
            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
            "sender": "@alice:example.org",
            "state_key": "@alice:example.org",
            "unsigned": {
                "prev_content": {
                    "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
                    "displayname": "Alice Margatroid",
                    "is_direct": true,
                    "membership": "invite",
                    "third_party_invite": {
                        "display_name": "alice",
                        "signed": {
                            "mxid": "@alice:example.org",
                            "signatures": {
                                "magic.forest": {
                                    "ed25519:3": "foobar",
                                },
                            },
                            "token": "abc123"
                        },
                    },
                },
            },
        });

        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
        assert_eq!(ev.sender, "@alice:example.org");
        assert_eq!(ev.state_key, "@alice:example.org");

        assert_eq!(ev.content.avatar_url, None);
        assert_eq!(ev.content.displayname, None);
        assert_eq!(ev.content.is_direct, None);
        assert_eq!(ev.content.membership, MembershipState::Join);
        assert_matches!(ev.content.third_party_invite, None);

        let prev_content = ev.unsigned.prev_content.unwrap();
        assert_eq!(
            prev_content.avatar_url.as_deref(),
            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
        );
        assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
        assert_eq!(prev_content.is_direct, Some(true));
        assert_eq!(prev_content.membership, MembershipState::Invite);

        let third_party_invite = prev_content.third_party_invite.unwrap();
        assert_eq!(third_party_invite.display_name, "alice");
        assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
        assert_eq!(
            third_party_invite.signed.signatures,
            btreemap! {
                server_name!("magic.forest").to_owned() => btreemap! {
                    server_signing_key_id!("ed25519:3").to_owned() => "foobar".to_owned()
                }
            }
        );
        assert_eq!(third_party_invite.signed.token, "abc123");
    }

    #[test]
    fn serde_with_join_authorized() {
        let json = json!({
            "type": "m.room.member",
            "content": {
                "membership": "join",
                "join_authorised_via_users_server": "@notcarl:example.com"
            },
            "event_id": "$h29iv0s8:example.com",
            "origin_server_ts": 1,
            "room_id": "!n8f893n9:example.com",
            "sender": "@carl:example.com",
            "state_key": "@carl:example.com"
        });

        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
        assert_eq!(ev.room_id, "!n8f893n9:example.com");
        assert_eq!(ev.sender, "@carl:example.com");
        assert_eq!(ev.state_key, "@carl:example.com");
        assert!(ev.unsigned.is_empty());

        assert_eq!(ev.content.avatar_url, None);
        assert_eq!(ev.content.displayname, None);
        assert_eq!(ev.content.is_direct, None);
        assert_eq!(ev.content.membership, MembershipState::Join);
        assert_matches!(ev.content.third_party_invite, None);
        assert_eq!(
            ev.content.join_authorized_via_users_server.as_deref(),
            Some(user_id!("@notcarl:example.com"))
        );
    }
}
