{-# OPTIONS -Wno-ambiguous-fields #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

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"

isConvSystemMemberUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
isConvSystemMemberUpdateNotif n = fieldEquals n "payload.0.type" "conversation.system.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"

isConvCreateMeetingNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvCreateMeetingNotif n =
  fieldEquals n "payload.0.type" "conversation.create-meeting"

isConvDeleteMeetingNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvDeleteMeetingNotif n =
  fieldEquals n "payload.0.type" "conversation.delete-meeting"

isMeetingCreateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isMeetingCreateNotif n =
  fieldEquals n "payload.0.type" "meeting.create"

isMeetingMemberAddNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isMeetingMemberAddNotif n =
  fieldEquals n "payload.0.type" "meeting.member-add"

isMeetingUpdateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isMeetingUpdateNotif n =
  fieldEquals n "payload.0.type" "meeting.update"

isMeetingDeleteNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isMeetingDeleteNotif n =
  fieldEquals n "payload.0.type" "meeting.delete"

-- | 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"
    ||~ fieldEquals n "payload.0.type" "conversation.system.delete"

isConvAdminlessReminderNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvAdminlessReminderNotif n = fieldEquals n "payload.0.type" "conversation.adminless-reminder"

isConvSystemAdminlessReminderNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvSystemAdminlessReminderNotif n = fieldEquals n "payload.0.type" "conversation.system.adminless-reminder"

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"
