//! Constructors for [predefined push rules].
//!
//! [predefined push rules]: https://spec.matrix.org/v1.18/client-server-api/#predefined-rules

use ruma_macros::StringEnum;

use super::{
    Action::*, ConditionalPushRule, EventMatchConditionData, EventPropertyContainsConditionData,
    EventPropertyIsConditionData, HighlightTweakValue, PushCondition::*,
    RoomMemberCountConditionData, RoomMemberCountIs, RuleKind, Ruleset,
    SenderNotificationPermissionConditionData, SoundTweakValue, Tweak,
};
use crate::{PrivOwnedStr, UserId, power_levels::NotificationPowerLevelsKey};

impl Ruleset {
    /// The list of all [predefined push rules].
    ///
    /// # Parameters
    ///
    /// - `user_id`: the user for which to generate the default rules. Some rules depend on the
    ///   user's ID (for instance those to send notifications when they are mentioned).
    ///
    /// [predefined push rules]: https://spec.matrix.org/v1.18/client-server-api/#predefined-rules
    pub fn server_default(user_id: &UserId) -> Self {
        Self {
            override_: [
                ConditionalPushRule::master(),
                ConditionalPushRule::suppress_notices(),
                ConditionalPushRule::invite_for_me(user_id),
                ConditionalPushRule::member_event(),
                ConditionalPushRule::is_user_mention(user_id),
                ConditionalPushRule::is_room_mention(),
                ConditionalPushRule::tombstone(),
                ConditionalPushRule::reaction(),
                ConditionalPushRule::server_acl(),
                ConditionalPushRule::suppress_edits(),
                #[cfg(feature = "unstable-msc3930")]
                ConditionalPushRule::poll_response(),
            ]
            .into(),
            #[cfg(feature = "unstable-msc4306")]
            postcontent: [
                ConditionalPushRule::unsubscribed_thread(),
                ConditionalPushRule::subscribed_thread(),
            ]
            .into(),
            underride: [
                ConditionalPushRule::call(),
                ConditionalPushRule::encrypted_room_one_to_one(),
                ConditionalPushRule::room_one_to_one(),
                ConditionalPushRule::message(),
                ConditionalPushRule::encrypted(),
                #[cfg(feature = "unstable-msc3930")]
                ConditionalPushRule::poll_start_one_to_one(),
                #[cfg(feature = "unstable-msc3930")]
                ConditionalPushRule::poll_start(),
                #[cfg(feature = "unstable-msc3930")]
                ConditionalPushRule::poll_end_one_to_one(),
                #[cfg(feature = "unstable-msc3930")]
                ConditionalPushRule::poll_end(),
            ]
            .into(),
            ..Default::default()
        }
    }

    /// Update this ruleset with the given server-default push rules.
    ///
    /// This will replace the server-default rules in this ruleset (with `default` set to `true`)
    /// with the given ones while keeping the `enabled` and `actions` fields in the same state.
    ///
    /// The default rules in this ruleset that are not in the new server-default rules are removed.
    ///
    /// # Parameters
    ///
    /// - `server_default`: the new server-default push rules. This ruleset must not contain
    ///   non-default rules.
    pub fn update_with_server_default(&mut self, mut new_server_default: Ruleset) {
        // Copy the default rules states from the old rules to the new rules and remove the
        // server-default rules from the old rules.
        macro_rules! copy_rules_state {
            ($new_ruleset:ident, $old_ruleset:ident, @fields $($field_name:ident),+) => {
                $(
                    $new_ruleset.$field_name = $new_ruleset
                        .$field_name
                        .into_iter()
                        .map(|mut new_rule| {
                            if let Some(old_rule) =
                                $old_ruleset.$field_name.shift_take(new_rule.rule_id.as_str())
                            {
                                new_rule.enabled = old_rule.enabled;
                                new_rule.actions = old_rule.actions;
                            }

                            new_rule
                        })
                        .collect();
                )+
            };
        }
        copy_rules_state!(new_server_default, self, @fields override_, content, room, sender, underride);

        // Remove the remaining server-default rules from the old rules.
        macro_rules! remove_remaining_default_rules {
            ($ruleset:ident, @fields $($field_name:ident),+) => {
                $(
                    $ruleset.$field_name.retain(|rule| !rule.default);
                )+
            };
        }
        remove_remaining_default_rules!(self, @fields override_, content, room, sender, underride);

        // `.m.rule.master` comes before all other push rules, while the other server-default push
        // rules come after.
        if let Some(master_rule) =
            new_server_default.override_.shift_take(PredefinedOverrideRuleId::Master.as_str())
        {
            let (pos, _) = self.override_.insert_full(master_rule);
            self.override_.move_index(pos, 0);
        }

        // Merge the new server-default rules into the old rules.
        macro_rules! merge_rules {
            ($old_ruleset:ident, $new_ruleset:ident, @fields $($field_name:ident),+) => {
                $(
                    $old_ruleset.$field_name.extend($new_ruleset.$field_name);
                )+
            };
        }
        merge_rules!(self, new_server_default, @fields override_, content, room, sender, underride);
    }
}

/// Default override push rules
impl ConditionalPushRule {
    /// Matches all events, this can be enabled to turn off all push notifications other than those
    /// generated by override rules set by the user.
    pub fn master() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: false,
            rule_id: PredefinedOverrideRuleId::Master.to_string(),
            conditions: vec![],
        }
    }

    /// Matches messages with a `msgtype` of `notice`.
    pub fn suppress_notices() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::SuppressNotices.to_string(),
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "content.msgtype".into(),
                "m.notice".into(),
            ))],
        }
    }

    /// Matches any invites to a new room for this user.
    pub fn invite_for_me(user_id: &UserId) -> Self {
        Self {
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::InviteForMe.to_string(),
            conditions: vec![
                EventMatch(EventMatchConditionData::new("type".into(), "m.room.member".into())),
                EventMatch(EventMatchConditionData::new(
                    "content.membership".into(),
                    "invite".into(),
                )),
                EventMatch(EventMatchConditionData::new("state_key".into(), user_id.to_string())),
            ],
        }
    }

    /// Matches any `m.room.member_event`.
    pub fn member_event() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::MemberEvent.to_string(),
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "type".into(),
                "m.room.member".into(),
            ))],
        }
    }

    /// Matches any message which contains the user’s Matrix ID in the list of `user_ids` under the
    /// `m.mentions` property.
    pub fn is_user_mention(user_id: &UserId) -> Self {
        Self {
            actions: vec![
                Notify,
                SetTweak(Tweak::Sound(SoundTweakValue::Default)),
                SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
            ],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::IsUserMention.to_string(),
            conditions: vec![EventPropertyContains(EventPropertyContainsConditionData::new(
                r"content.m\.mentions.user_ids".to_owned(),
                user_id.as_str().into(),
            ))],
        }
    }

    /// Matches any state event whose type is `m.room.tombstone`. This
    /// is intended to notify users of a room when it is upgraded,
    /// similar to what an `@room` notification would accomplish.
    pub fn tombstone() -> Self {
        Self {
            actions: vec![Notify, SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::Tombstone.to_string(),
            conditions: vec![
                EventMatch(EventMatchConditionData::new("type".into(), "m.room.tombstone".into())),
                EventMatch(EventMatchConditionData::new("state_key".into(), "".into())),
            ],
        }
    }

    /// Matches any message from a sender with the proper power level with the `room` property of
    /// the `m.mentions` property set to `true`.
    pub fn is_room_mention() -> Self {
        Self {
            actions: vec![Notify, SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::IsRoomMention.to_string(),
            conditions: vec![
                EventPropertyIs(EventPropertyIsConditionData::new(
                    r"content.m\.mentions.room".to_owned(),
                    true.into(),
                )),
                SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
                    NotificationPowerLevelsKey::Room,
                )),
            ],
        }
    }

    /// Matches [reactions] to a message.
    ///
    /// [reactions]: https://spec.matrix.org/v1.18/client-server-api/#event-annotations-and-reactions
    pub fn reaction() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::Reaction.to_string(),
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "type".into(),
                "m.reaction".into(),
            ))],
        }
    }

    /// Matches [room server ACLs].
    ///
    /// [room server ACLs]: https://spec.matrix.org/v1.18/client-server-api/#server-access-control-lists-acls-for-rooms
    pub fn server_acl() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::RoomServerAcl.to_string(),
            conditions: vec![
                EventMatch(EventMatchConditionData::new("type".into(), "m.room.server_acl".into())),
                EventMatch(EventMatchConditionData::new("state_key".into(), "".into())),
            ],
        }
    }

    /// Matches [event replacements].
    ///
    /// [event replacements]: https://spec.matrix.org/v1.18/client-server-api/#event-replacements
    pub fn suppress_edits() -> Self {
        Self {
            actions: vec![],
            default: true,
            enabled: true,
            rule_id: PredefinedOverrideRuleId::SuppressEdits.to_string(),
            conditions: vec![EventPropertyIs(EventPropertyIsConditionData::new(
                r"content.m\.relates_to.rel_type".to_owned(),
                "m.replace".into(),
            ))],
        }
    }

    /// Matches a poll response event sent in any room.
    ///
    /// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
    ///
    /// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    pub fn poll_response() -> Self {
        Self {
            rule_id: PredefinedOverrideRuleId::PollResponse.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventPropertyIs(EventPropertyIsConditionData::new(
                "type".to_owned(),
                "org.matrix.msc3381.poll.response".into(),
            ))],
            actions: vec![],
        }
    }
}

/// Default underrides push rules
impl ConditionalPushRule {
    /// Matches any incoming VOIP call.
    pub fn call() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::Call.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "type".into(),
                "m.call.invite".into(),
            ))],
            actions: vec![Notify, SetTweak(Tweak::Sound("ring".into()))],
        }
    }

    /// Matches any encrypted event sent in a room with exactly two members.
    ///
    /// Unlike other push rules, this rule cannot be matched against the content of the event by
    /// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it
    /// either matches all events that are encrypted (in 1:1 rooms) or none.
    pub fn encrypted_room_one_to_one() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::EncryptedRoomOneToOne.to_string(),
            default: true,
            enabled: true,
            conditions: vec![
                RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs::from(
                    js_int::uint!(2),
                ))),
                EventMatch(EventMatchConditionData::new("type".into(), "m.room.encrypted".into())),
            ],
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
        }
    }

    /// Matches any message sent in a room with exactly two members.
    pub fn room_one_to_one() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::RoomOneToOne.to_string(),
            default: true,
            enabled: true,
            conditions: vec![
                RoomMemberCount(RoomMemberCountConditionData::new(js_int::uint!(2).into())),
                EventMatch(EventMatchConditionData::new("type".into(), "m.room.message".into())),
            ],
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
        }
    }

    /// Matches all chat messages.
    pub fn message() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::Message.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "type".into(),
                "m.room.message".into(),
            ))],
            actions: vec![Notify],
        }
    }

    /// Matches all encrypted events.
    ///
    /// Unlike other push rules, this rule cannot be matched against the content of the event by
    /// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it
    /// either matches all events that are encrypted (in group rooms) or none.
    pub fn encrypted() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::Encrypted.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventMatch(EventMatchConditionData::new(
                "type".into(),
                "m.room.encrypted".into(),
            ))],
            actions: vec![Notify],
        }
    }

    /// Matches a poll start event sent in a room with exactly two members.
    ///
    /// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
    ///
    /// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    pub fn poll_start_one_to_one() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::PollStartOneToOne.to_string(),
            default: true,
            enabled: true,
            conditions: vec![
                RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs::from(
                    js_int::uint!(2),
                ))),
                EventPropertyIs(EventPropertyIsConditionData::new(
                    "type".to_owned(),
                    "org.matrix.msc3381.poll.start".into(),
                )),
            ],
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
        }
    }

    /// Matches a poll start event sent in any room.
    ///
    /// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
    ///
    /// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    pub fn poll_start() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::PollStart.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventPropertyIs(EventPropertyIsConditionData::new(
                "type".to_owned(),
                "org.matrix.msc3381.poll.start".into(),
            ))],
            actions: vec![Notify],
        }
    }

    /// Matches a poll end event sent in a room with exactly two members.
    ///
    /// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
    ///
    /// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    pub fn poll_end_one_to_one() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::PollEndOneToOne.to_string(),
            default: true,
            enabled: true,
            conditions: vec![
                RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs::from(
                    js_int::uint!(2),
                ))),
                EventPropertyIs(EventPropertyIsConditionData::new(
                    "type".to_owned(),
                    "org.matrix.msc3381.poll.end".into(),
                )),
            ],
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
        }
    }

    /// Matches a poll end event sent in any room.
    ///
    /// This rule uses the unstable prefixes defined in [MSC3381] and [MSC3930].
    ///
    /// [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    pub fn poll_end() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::PollEnd.to_string(),
            default: true,
            enabled: true,
            conditions: vec![EventPropertyIs(EventPropertyIsConditionData::new(
                "type".to_owned(),
                "org.matrix.msc3381.poll.end".into(),
            ))],
            actions: vec![Notify],
        }
    }

    /// Matches an event that's part of a thread, that is *not* subscribed to, by the current user.
    ///
    /// Thread subscriptions are defined in [MSC4306].
    ///
    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
    #[cfg(feature = "unstable-msc4306")]
    pub fn unsubscribed_thread() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::UnsubscribedThread.to_string(),
            default: true,
            enabled: true,
            conditions: vec![ThreadSubscription(super::ThreadSubscriptionConditionData::new(
                false,
            ))],
            actions: vec![],
        }
    }

    /// Matches an event that's part of a thread, that *is* subscribed to, by the current user.
    ///
    /// Thread subscriptions are defined in [MSC4306].
    ///
    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
    #[cfg(feature = "unstable-msc4306")]
    pub fn subscribed_thread() -> Self {
        Self {
            rule_id: PredefinedUnderrideRuleId::SubscribedThread.to_string(),
            default: true,
            enabled: true,
            conditions: vec![ThreadSubscription(super::ThreadSubscriptionConditionData::new(true))],
            actions: vec![Notify, SetTweak(Tweak::Sound(SoundTweakValue::Default))],
        }
    }
}

/// The rule IDs of the predefined server push rules.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum PredefinedRuleId {
    /// User-configured rules that override all other kinds.
    Override(PredefinedOverrideRuleId),

    /// Lowest priority user-defined rules.
    Underride(PredefinedUnderrideRuleId),

    /// Content-specific rules.
    Content(PredefinedContentRuleId),
}

impl PredefinedRuleId {
    /// Creates a string slice from this `PredefinedRuleId`.
    pub fn as_str(&self) -> &str {
        match self {
            Self::Override(id) => id.as_str(),
            Self::Underride(id) => id.as_str(),
            Self::Content(id) => id.as_str(),
        }
    }

    /// Get the kind of this `PredefinedRuleId`.
    pub fn kind(&self) -> RuleKind {
        match self {
            Self::Override(id) => id.kind(),
            Self::Underride(id) => id.kind(),
            Self::Content(id) => id.kind(),
        }
    }
}

impl AsRef<str> for PredefinedRuleId {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

/// The rule IDs of the predefined override server push rules.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[ruma_enum(rename_all(prefix = ".m.rule.", rule = "snake_case"))]
#[non_exhaustive]
pub enum PredefinedOverrideRuleId {
    /// `.m.rule.master`
    Master,

    /// `.m.rule.suppress_notices`
    SuppressNotices,

    /// `.m.rule.invite_for_me`
    InviteForMe,

    /// `.m.rule.member_event`
    MemberEvent,

    /// `.m.rule.is_user_mention`
    IsUserMention,

    /// `.m.rule.contains_display_name`
    #[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
    ContainsDisplayName,

    /// `.m.rule.is_room_mention`
    IsRoomMention,

    /// `.m.rule.roomnotif`
    #[ruma_enum(rename = ".m.rule.roomnotif")]
    #[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsRoomMention instead."]
    RoomNotif,

    /// `.m.rule.tombstone`
    Tombstone,

    /// `.m.rule.reaction`
    Reaction,

    /// `.m.rule.room.server_acl`
    #[ruma_enum(rename = ".m.rule.room.server_acl")]
    RoomServerAcl,

    /// `.m.rule.suppress_edits`
    SuppressEdits,

    /// `.m.rule.poll_response`
    ///
    /// This uses the unstable prefix defined in [MSC3930].
    ///
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    #[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_response")]
    PollResponse,

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

impl PredefinedOverrideRuleId {
    /// Get the kind of this `PredefinedOverrideRuleId`.
    pub fn kind(&self) -> RuleKind {
        RuleKind::Override
    }
}

/// The rule IDs of the predefined underride server push rules.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[ruma_enum(rename_all(prefix = ".m.rule.", rule = "snake_case"))]
#[non_exhaustive]
pub enum PredefinedUnderrideRuleId {
    /// `.m.rule.call`
    Call,

    /// `.m.rule.encrypted_room_one_to_one`
    EncryptedRoomOneToOne,

    /// `.m.rule.room_one_to_one`
    RoomOneToOne,

    /// `.m.rule.message`
    Message,

    /// `.m.rule.encrypted`
    Encrypted,

    /// `.m.rule.poll_start_one_to_one`
    ///
    /// This uses the unstable prefix defined in [MSC3930].
    ///
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    #[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_start_one_to_one")]
    PollStartOneToOne,

    /// `.m.rule.poll_start`
    ///
    /// This uses the unstable prefix defined in [MSC3930].
    ///
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    #[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_start")]
    PollStart,

    /// `.m.rule.poll_end_one_to_one`
    ///
    /// This uses the unstable prefix defined in [MSC3930].
    ///
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    #[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_end_one_to_one")]
    PollEndOneToOne,

    /// `.m.rule.poll_end`
    ///
    /// This uses the unstable prefix defined in [MSC3930].
    ///
    /// [MSC3930]: https://github.com/matrix-org/matrix-spec-proposals/pull/3930
    #[cfg(feature = "unstable-msc3930")]
    #[ruma_enum(rename = ".org.matrix.msc3930.rule.poll_end")]
    PollEnd,

    /// `.m.rule.unsubscribed_thread`
    ///
    /// This uses the unstable prefix defined in [MSC4306].
    ///
    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
    #[cfg(feature = "unstable-msc4306")]
    #[ruma_enum(rename = ".io.element.msc4306.rule.unsubscribed_thread")]
    UnsubscribedThread,

    /// `.m.rule.subscribed_thread`
    ///
    /// This uses the unstable prefix defined in [MSC4306].
    ///
    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
    #[cfg(feature = "unstable-msc4306")]
    #[ruma_enum(rename = ".io.element.msc4306.rule.subscribed_thread")]
    SubscribedThread,

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

impl PredefinedUnderrideRuleId {
    /// Get the kind of this `PredefinedUnderrideRuleId`.
    pub fn kind(&self) -> RuleKind {
        RuleKind::Underride
    }
}

/// The rule IDs of the predefined content server push rules.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[ruma_enum(rename_all(prefix = ".m.rule.", rule = "snake_case"))]
#[non_exhaustive]
pub enum PredefinedContentRuleId {
    /// `.m.rule.contains_user_name`
    #[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
    ContainsUserName,

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

impl PredefinedContentRuleId {
    /// Get the kind of this `PredefinedContentRuleId`.
    pub fn kind(&self) -> RuleKind {
        RuleKind::Content
    }
}

#[cfg(test)]
mod tests {
    use assert_matches2::assert_matches;
    use assign::assign;

    use super::PredefinedOverrideRuleId;
    use crate::{
        push::{Action, ConditionalPushRule, ConditionalPushRuleInit, Ruleset},
        user_id,
    };

    #[test]
    fn update_with_server_default() {
        let user_rule_id = "user_always_true";
        let default_rule_id = ".default_always_true";

        let override_ = [
            // Default `.m.rule.master` push rule with non-default state.
            assign!(ConditionalPushRule::master(), { enabled: true, actions: vec![Action::Notify]}),
            // User-defined push rule.
            ConditionalPushRuleInit {
                actions: vec![],
                default: false,
                enabled: false,
                rule_id: user_rule_id.to_owned(),
                conditions: vec![],
            }
            .into(),
            // Old server-default push rule.
            ConditionalPushRuleInit {
                actions: vec![],
                default: true,
                enabled: true,
                rule_id: default_rule_id.to_owned(),
                conditions: vec![],
            }
            .into(),
        ]
        .into_iter()
        .collect();
        let mut ruleset = Ruleset { override_, ..Default::default() };

        let new_server_default = Ruleset::server_default(user_id!("@user:localhost"));

        ruleset.update_with_server_default(new_server_default);

        // Master rule is in first position.
        let master_rule = &ruleset.override_[0];
        assert_eq!(master_rule.rule_id, PredefinedOverrideRuleId::Master.as_str());

        // `enabled` and `actions` have been copied from the old rules.
        assert!(master_rule.enabled);
        assert_eq!(master_rule.actions.len(), 1);
        assert_matches!(&master_rule.actions[0], Action::Notify);

        // Non-server-default rule is still present and hasn't changed.
        let user_rule = ruleset.override_.get(user_rule_id).unwrap();
        assert!(!user_rule.enabled);
        assert_eq!(user_rule.actions.len(), 0);

        // Old server-default rule is gone.
        assert_matches!(ruleset.override_.get(default_rule_id), None);

        // New server-default rule is present and hasn't changed.
        let member_event_rule =
            ruleset.override_.get(PredefinedOverrideRuleId::MemberEvent.as_str()).unwrap();
        assert!(member_event_rule.enabled);
        assert_eq!(member_event_rule.actions.len(), 0);
    }
}
