use js_option::JsOption;
use ruma_common::serde::from_raw_json_value;
use serde::{Deserialize, Serialize, de, ser::SerializeStruct};
use serde_json::value::RawValue as RawJsonValue;

use super::v3::{PusherAction, PusherPostData};

#[derive(Debug, Deserialize)]
struct PusherPostDataDeHelper {
    #[serde(default)]
    append: bool,
}

impl<'de> Deserialize<'de> for PusherPostData {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        let json = Box::<RawJsonValue>::deserialize(deserializer)?;

        let PusherPostDataDeHelper { append } = from_raw_json_value(&json)?;
        let pusher = from_raw_json_value(&json)?;

        Ok(Self { pusher, append })
    }
}

impl Serialize for PusherAction {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            PusherAction::Post(pusher) => pusher.serialize(serializer),
            PusherAction::Delete(ids) => {
                let mut st = serializer.serialize_struct("PusherAction", 3)?;
                st.serialize_field("pushkey", &ids.pushkey)?;
                st.serialize_field("app_id", &ids.app_id)?;
                st.serialize_field("kind", &None::<&str>)?;
                st.end()
            }
        }
    }
}

#[derive(Debug, Deserialize)]
struct PusherActionDeHelper {
    kind: JsOption<String>,
}

impl<'de> Deserialize<'de> for PusherAction {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
        let PusherActionDeHelper { kind } = from_raw_json_value(&json)?;

        match kind {
            JsOption::Some(_) => Ok(Self::Post(from_raw_json_value(&json)?)),
            JsOption::Null => Ok(Self::Delete(from_raw_json_value(&json)?)),
            // This is unreachable because we don't use `#[serde(default)]` on the field.
            JsOption::Undefined => Err(de::Error::missing_field("kind")),
        }
    }
}

#[cfg(test)]
mod tests {
    use assert_matches2::{assert_let, assert_matches};
    use ruma_common::canonical_json::assert_to_canonical_json_eq;
    use serde_json::{from_value as from_json_value, json};

    use super::PusherAction;
    use crate::push::{
        EmailPusherData, Pusher, PusherIds, PusherKind, set_pusher::v3::PusherPostData,
    };

    #[test]
    fn serialize_post() {
        let action = PusherAction::Post(PusherPostData {
            pusher: Pusher {
                ids: PusherIds::new("abcdef".to_owned(), "my.matrix.app".to_owned()),
                kind: PusherKind::Email(EmailPusherData::new()),
                app_display_name: "My Matrix App".to_owned(),
                device_display_name: "My Phone".to_owned(),
                profile_tag: None,
                lang: "en".to_owned(),
            },
            append: false,
        });

        assert_to_canonical_json_eq!(
            action,
            json!({
                "pushkey": "abcdef",
                "app_id": "my.matrix.app",
                "kind": "email",
                "app_display_name": "My Matrix App",
                "device_display_name": "My Phone",
                "lang": "en",
                "data": {}
            })
        );
    }

    #[test]
    fn serialize_delete() {
        let action =
            PusherAction::Delete(PusherIds::new("abcdef".to_owned(), "my.matrix.app".to_owned()));

        assert_to_canonical_json_eq!(
            action,
            json!({
                "pushkey": "abcdef",
                "app_id": "my.matrix.app",
                "kind": null,
            })
        );
    }

    #[test]
    fn deserialize_post() {
        let json = json!({
            "pushkey": "abcdef",
            "app_id": "my.matrix.app",
            "kind": "email",
            "app_display_name": "My Matrix App",
            "device_display_name": "My Phone",
            "lang": "en",
            "data": {}
        });

        assert_let!(PusherAction::Post(post_data) = from_json_value(json).unwrap());
        assert!(!post_data.append);

        let pusher = post_data.pusher;
        assert_eq!(pusher.ids.pushkey, "abcdef");
        assert_eq!(pusher.ids.app_id, "my.matrix.app");
        assert_matches!(pusher.kind, PusherKind::Email(_));
        assert_eq!(pusher.app_display_name, "My Matrix App");
        assert_eq!(pusher.device_display_name, "My Phone");
        assert_eq!(pusher.profile_tag, None);
        assert_eq!(pusher.lang, "en");
    }

    #[test]
    fn deserialize_delete() {
        let json = json!({
            "pushkey": "abcdef",
            "app_id": "my.matrix.app",
            "kind": null,
        });

        assert_let!(PusherAction::Delete(ids) = from_json_value(json).unwrap());
        assert_eq!(ids.pushkey, "abcdef");
        assert_eq!(ids.app_id, "my.matrix.app");
    }
}
