{-# 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 Test.Notifications where

import API.Brig
import API.Common
import API.Gundeck
import API.GundeckInternal
import Data.Time (UTCTime)
import Data.Time.Format.ISO8601 (iso8601ParseM)
import Notifications
import SetupHelpers
import Testlib.Prelude

examplePush :: (MakesValue u) => u -> App Value
examplePush :: forall u. MakesValue u => u -> App Value
examplePush u
u = do
  r <- u -> App Value
forall u. MakesValue u => u -> App Value
recipient u
u
  pure
    $ object
      [ "recipients" .= [r],
        "payload" .= [object ["hello" .= "world"]]
      ]

testFetchAllNotifications :: App ()
testFetchAllNotifications :: App ()
testFetchAllNotifications = do
  user <- Domain -> App Value
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> App Value
randomUserId Domain
OwnDomain
  push <- examplePush user

  let n = Int
10
  replicateM_ n
    $ bindResponse (postPush user [push])
    $ \Response
res ->
      Response
res.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200

  let c :: Maybe String = Just "deadbeef"
  ns <- getNotifications user (def {client = c} :: GetNotifications) >>= getJSON 200

  expected <- replicateM n (push %. "payload")
  allNotifs <- ns %. "notifications" & asList
  actual <- traverse (%. "payload") allNotifs
  actual `shouldMatch` expected

  firstNotif <-
    getNotification
      user
      (def {client = c} :: GetNotification)
      (head allNotifs %. "id")
      >>= getJSON 200
  firstNotif `shouldMatch` head allNotifs

  lastNotif <-
    getLastNotification
      user
      (def {client = c} :: GetNotification)
      >>= getJSON 200
  lastNotif `shouldMatch` last allNotifs

testLastNotification :: App ()
testLastNotification :: App ()
testLastNotification = do
  user <- Domain -> App Value
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> App Value
randomUserId Domain
OwnDomain
  userId <- user %. "id" & asString
  let push String
c =
        [Pair] -> Value
object
          [ String
"recipients"
              String -> [Value] -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [ [Pair] -> Value
object
                     [ String
"user_id" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
userId,
                       String
"route" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"any",
                       String
"clients" String -> [String] -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [String
c]
                     ]
                 ],
            String
"payload" String -> [Value] -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [[Pair] -> Value
object [String
"client" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
c]]
          ]

  for_ ["a", "b", "c", "d", "e", "f"] $ \String
c ->
    App Response -> (Response -> App ()) -> App ()
forall a.
HasCallStack =>
App Response -> (Response -> App a) -> App a
bindResponse (Value -> [Value] -> App Response
forall user a.
(HasCallStack, MakesValue user, MakesValue a) =>
user -> [a] -> App Response
postPush Value
user [String -> Value
push String
c]) ((Response -> App ()) -> App ()) -> (Response -> App ()) -> App ()
forall a b. (a -> b) -> a -> b
$ \Response
res ->
      Response
res.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200

  lastNotif <- getLastNotification user def {client = Just "c"} >>= getJSON 200
  lastNotif %. "payload" `shouldMatch` [object ["client" .= "c"]]

testInvalidNotification :: (HasCallStack) => App ()
testInvalidNotification :: HasCallStack => App ()
testInvalidNotification = do
  user <- Domain -> App Value
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> App Value
randomUserId Domain
OwnDomain

  -- test uuid v4 as "since"
  do
    notifId <- randomId
    void
      $ getNotifications user def {since = Just notifId}
      >>= getJSON 400

  -- test arbitrary uuid v1 as "since"
  do
    notifId <- randomUUIDv1
    void
      $ getNotifications user def {since = Just notifId}
      >>= getJSON 404

-- | Check that client-add notifications use the V6 format:
-- @
--   "capabilities": { "capabilities": [..] }
-- @
--
-- Migration plan: clients must be able to parse both old and new schema starting from V7.  Once V6 is deprecated, the backend can start sending notifications in the new form.
testAddClientNotification :: (HasCallStack) => App ()
testAddClientNotification :: HasCallStack => App ()
testAddClientNotification = do
  alice <- Domain -> CreateUser -> App Value
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> CreateUser -> App Value
randomUser Domain
OwnDomain CreateUser
forall a. Default a => a
def

  e <- withWebSocket alice $ \WebSocket
ws -> do
    App Response -> App ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (App Response -> App ()) -> App Response -> App ()
forall a b. (a -> b) -> a -> b
$ Value -> AddClient -> App Response
forall user.
(HasCallStack, MakesValue user) =>
user -> AddClient -> App Response
addClient Value
alice AddClient
forall a. Default a => a
def
    n <- HasCallStack => (Value -> App Bool) -> WebSocket -> App Value
(Value -> App Bool) -> WebSocket -> App Value
awaitMatch Value -> App Bool
forall a. (HasCallStack, MakesValue a) => a -> App Bool
isUserClientAddNotif WebSocket
ws
    nPayload n

  void $ e %. "client.capabilities.capabilities" & asList

testGetServerTime :: (HasCallStack) => App ()
testGetServerTime :: HasCallStack => App ()
testGetServerTime = do
  user <- Domain -> CreateUser -> App Value
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> CreateUser -> App Value
randomUser Domain
OwnDomain CreateUser
forall a. Default a => a
def
  formattedTimestampStr <-
    getServerTime user `bindResponse` \Response
r -> do
      Response
r.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
      Response
r.json App Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"time" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString
  void $ assertJust ("expected ISO 8601 format, but got: " <> formattedTimestampStr) $ iso8601ParseM @Maybe @UTCTime formattedTimestampStr