content: {-# OPTIONS -Wno-ambiguous-fields #-}
  module Notifications where
  
  import API.Gundeck
  import Control.Error (lastMay)
  import Control.Monad.Extra
  import Control.Monad.Reader (asks)
  import Testlib.Prelude
  import UnliftIO (timeout)
  import UnliftIO.Concurrent
  
  -- | assert that no notifications with the predicate happen within the timeout
  assertNoNotifications ::
    (HasCallStack, MakesValue user, MakesValue client) =>
    -- | the user
    user ->
    -- | the client of that user
    client ->
    -- | the last notif
    Maybe String ->
    -- | the predicate
    (Value -> App Bool) ->
    App ()
  assertNoNotifications u uc since0 p = do
    ucid <- objId uc
    let go since = do
          notifs <-
            getNotifications u def {client = Just ucid, since = since}
              `bindResponse` asList
                . (%. "notifications")
                . (.json)
          partitionM p notifs >>= \case
            ([], nonMatching) ->
              threadDelay 1_000 *> case nonMatching of
                (lastMay -> Just lst) -> objId lst >>= go . Just
                _ -> go Nothing
            (matching, _) -> do
              pj <- prettyJSON matching
              assertFailure
                $ unlines
                  [ "Expected no matching events  but got:",
                    pj
                  ]
    Nothing <- asks timeOutSeconds >>= flip timeout (go since0)
    pure ()
  
  awaitNotifications ::
    (HasCallStack, MakesValue user, MakesValue client) =>
    user ->
    Maybe client ->
    Maybe String ->
    -- | Max no. of notifications
    Int ->
    -- | Selection function. Should not throw any exceptions
    (Value -> App Bool) ->
    App [Value]
  awaitNotifications user client since0 n selector = do
    tSecs <- asks ((* 1000) . timeOutSeconds)
    assertAwaitResult =<< go tSecs since0 (AwaitResult False n [] [])
    where
      go timeRemaining since res0
        | timeRemaining <= 0 = pure res0
        | otherwise =
            do
              c <- for client (asString . make)
              notifs <-
                getNotifications
                  user
                  def {since = since, client = c}
                  `bindResponse` \resp -> asList (resp.json %. "notifications")
              lastNotifId <- case notifs of
                [] -> pure since
                _ -> Just <$> objId (last notifs)
              (matching, notMatching) <- partitionM selector notifs
              let matchesSoFar = res0.matches <> matching
                  res =
                    res0
                      { matches = matchesSoFar,
                        nonMatches = res0.nonMatches <> notMatching,
                        success = length matchesSoFar >= res0.nMatchesExpected
                      }
              if res.success
                then pure res
                else do
                  threadDelay 1_000
                  go (timeRemaining - 1) lastNotifId res
  
  awaitNotificationClient ::
    (HasCallStack, MakesValue user, MakesValue client, MakesValue lastNotifId) =>
    user ->
    client ->
    Maybe lastNotifId ->
    (Value -> App Bool) ->
    App Value
  awaitNotificationClient user client lastNotifId selector = do
    since0 <- mapM objId lastNotifId
    head <$> awaitNotifications user (Just client) since0 1 selector
  
  awaitNotification ::
    (HasCallStack, MakesValue user, MakesValue lastNotifId) =>
    user ->
    Maybe lastNotifId ->
    (Value -> App Bool) ->
    App Value
  awaitNotification user lastNotifId selector = do
    since0 <- mapM objId lastNotifId
    head <$> awaitNotifications user (Nothing :: Maybe ()) since0 1 selector
  
  isDeleteUserNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isDeleteUserNotif n =
    nPayload n %. "type" `isEqual` "user.delete"
  
  isFeatureConfigUpdateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isFeatureConfigUpdateNotif n =
    nPayload n %. "type" `isEqual` "feature-config.update"
  
  isNewMessageNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isNewMessageNotif n = fieldEquals n "payload.0.type" "conversation.otr-message-add"
  
  isNewMLSMessageNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isNewMLSMessageNotif n = fieldEquals n "payload.0.type" "conversation.mls-message-add"
  
  isWelcomeNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isWelcomeNotif n = fieldEquals n "payload.0.type" "conversation.mls-welcome"
  
  isMemberJoinNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join"
  
  isConvLeaveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave"
  
  isConvLeaveNotifWithLeaver :: (HasCallStack, MakesValue user, MakesValue a) => user -> a -> App Bool
  isConvLeaveNotifWithLeaver user n =
    fieldEquals n "payload.0.type" "conversation.member-leave"
      &&~ (n %. "payload.0.data.user_ids.0") `isEqual` (user %. "id")
  
  isNotifConv :: (HasCallStack, MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool
  isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv)
  
  isNotifConvId :: (HasCallStack, MakesValue a, HasCallStack) => ConvId -> a -> App Bool
  isNotifConvId conv n = do
    let subconvField = "payload.0.subconv"
    fieldEquals n "payload.0.qualified_conversation" (convIdToQidObject conv)
      &&~ maybe (isNothing <$> lookupField n subconvField) (fieldEquals n subconvField) conv.subconvId
  
  isNotifForUser :: (HasCallStack, MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
  isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user)
  
  isNotifFromUser :: (HasCallStack, MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
  isNotifFromUser user n = fieldEquals n "payload.0.qualified_from" (objQidObject user)
  
  isConvNameChangeNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isConvNameChangeNotif n = fieldEquals n "payload.0.type" "conversation.rename"
  
  isMemberUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
  isMemberUpdateNotif n = fieldEquals n "payload.0.type" "conversation.member-update"
  
  isReceiptModeUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
  isReceiptModeUpdateNotif n =
    fieldEquals n "payload.0.type" "conversation.receipt-mode-update"
  
  isChannelAddPermissionUpdate :: (HasCallStack, MakesValue n) => n -> App Bool
  isChannelAddPermissionUpdate n =
    fieldEquals n "payload.0.type" "conversation.add-permission-update"
  
  isConvMsgTimerUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
  isConvMsgTimerUpdateNotif n =
    fieldEquals n "payload.0.type" "conversation.message-timer-update"
  
  isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
  isConvAccessUpdateNotif n =
    fieldEquals n "payload.0.type" "conversation.access-update"
  
  isConvCreateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create"
  
  -- | like 'isConvCreateNotif' but excludes self conversations
  isConvCreateNotifNotSelf :: (HasCallStack, MakesValue a) => a -> App Bool
  isConvCreateNotifNotSelf n =
    fieldEquals n "payload.0.type" "conversation.create"
      &&~ do not <$> fieldEquals n "payload.0.data.access" ["private"]
  
  isConvDeleteNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete"
  
  notifTypeIsEqual :: (HasCallStack, MakesValue a) => String -> a -> App Bool
  notifTypeIsEqual typ n = nPayload n %. "type" `isEqual` typ
  
  isTeamMemberJoinNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isTeamMemberJoinNotif = notifTypeIsEqual "team.member-join"
  
  isTeamMemberLeaveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isTeamMemberLeaveNotif = notifTypeIsEqual "team.member-leave"
  
  isTeamCollaboratorAddedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isTeamCollaboratorAddedNotif = notifTypeIsEqual "team.collaborator-add"
  
  isTeamCollaboratorRemovedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isTeamCollaboratorRemovedNotif = notifTypeIsEqual "team.collaborator-remove"
  
  isUserActivateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserActivateNotif = notifTypeIsEqual "user.activate"
  
  isUserClientAddNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserClientAddNotif = notifTypeIsEqual "user.client-add"
  
  isUserUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserUpdatedNotif = notifTypeIsEqual "user.update"
  
  isUserClientRemoveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove"
  
  isUserLegalholdRequestNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserLegalholdRequestNotif = notifTypeIsEqual "user.legalhold-request"
  
  isUserLegalholdEnabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserLegalholdEnabledNotif = notifTypeIsEqual "user.legalhold-enable"
  
  isUserLegalholdDisabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserLegalholdDisabledNotif = notifTypeIsEqual "user.legalhold-disable"
  
  isUserConnectionNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserConnectionNotif = notifTypeIsEqual "user.connection"
  
  isConnectionNotif :: (HasCallStack, MakesValue a) => String -> a -> App Bool
  isConnectionNotif status n =
    -- NB:
    -- (&&) <$> (print "hello" *> pure False) <*> fail "bla" === _|_
    -- runMaybeT $  (lift (print "hello") *> MaybeT (pure Nothing)) *> lift (fail "bla") === pure Nothing
    nPayload n %. "type" `isEqual` "user.connection"
      &&~ nPayload n %. "connection.status" `isEqual` status
  
  isUserGroupCreatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserGroupCreatedNotif = notifTypeIsEqual "user-group.created"
  
  isUserGroupUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
  isUserGroupUpdatedNotif = notifTypeIsEqual "user-group.updated"
  
  isConvResetNotif :: (HasCallStack, MakesValue n) => n -> App Bool
  isConvResetNotif n =
    fieldEquals n "payload.0.type" "conversation.mls-reset"
  
  assertLeaveNotification ::
    ( HasCallStack,
      MakesValue fromUser,
      MakesValue conv,
      MakesValue user,
      MakesValue kickedUser
    ) =>
    fromUser ->
    conv ->
    user ->
    String ->
    kickedUser ->
    App ()
  assertLeaveNotification fromUser conv user client leaver =
    void
      $ awaitNotificationClient
        user
        (Just client)
        noValue
        ( allPreds
            [ isConvLeaveNotif,
              isNotifConv conv,
              isNotifForUser leaver,
              isNotifFromUser fromUser
            ]
        )
  
  assertConvUserDeletedNotif :: (HasCallStack, MakesValue leaverId) => WebSocket -> leaverId -> App ()
  assertConvUserDeletedNotif ws leaverId = do
    n <- awaitMatch isConvLeaveNotif ws
    nPayload n %. "data.qualified_user_ids.0" `shouldMatch` leaverId
    nPayload n %. "data.reason" `shouldMatch` "user-deleted"
  