content: {-# LANGUAGE CPP #-}
  {-# LANGUAGE DuplicateRecordFields #-}
  {-# LANGUAGE FlexibleContexts #-}
  {-# LANGUAGE GADTs #-}
  {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE NamedFieldPuns #-}
  {-# LANGUAGE OverloadedLists #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE PatternSynonyms #-}
  {-# LANGUAGE QuasiQuotes #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE TupleSections #-}
  {-# LANGUAGE TypeApplications #-}
  {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
  
  module AgentTests.NotificationTests where
  
  -- import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging)
  import AgentTests.FunctionalAPITests
    ( agentCfgVPrevPQ,
      createConnection,
      exchangeGreetings,
      get,
      joinConnection,
      makeConnection,
      nGet,
      runRight,
      runRight_,
      sendMessage,
      switchComplete,
      testServerMatrix2,
      withAgent,
      withAgentClients2,
      withAgentClients3,
      withAgentClientsCfg2,
      withAgentClientsCfgServers2,
      (##>),
      (=##>),
      pattern CON,
      pattern CONF,
      pattern INFO,
      pattern Msg,
      pattern SENT,
    )
  import Control.Concurrent (ThreadId, killThread, threadDelay)
  import Control.Monad
  import Control.Monad.Except
  import Control.Monad.Reader (runReaderT)
  import Control.Monad.Trans.Except
  import qualified Data.Aeson as J
  import qualified Data.Aeson.Types as JT
  import Data.Bifunctor (bimap, first)
  import qualified Data.ByteString.Base64.URL as U
  import Data.ByteString.Char8 (ByteString)
  import qualified Data.ByteString.Char8 as B
  import Data.List.NonEmpty (NonEmpty (..))
  import qualified Data.List.NonEmpty as L
  import Data.Text.Encoding (encodeUtf8)
  import Data.Time.Clock.System (systemToUTCTime)
  import qualified Database.PostgreSQL.Simple as PSQL
  import NtfClient
  import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testNtfServer, testNtfServer2)
  import SMPClient
  import Simplex.Messaging.Agent hiding (checkNtfToken, createConnection, joinConnection, registerNtfToken, sendMessage, verifyNtfToken)
  import qualified Simplex.Messaging.Agent as A
  import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore')
  import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers)
  import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT)
  import Simplex.Messaging.Agent.Store.AgentStore (getSavedNtfToken)
  import Simplex.Messaging.Agent.Store.Common (withTransaction)
  import qualified Simplex.Messaging.Agent.Store.DB as DB
  import Simplex.Messaging.Agent.Store.Interface (closeDBStore, reopenDBStore)
  import Simplex.Messaging.Client (pattern NRMInteractive)
  import qualified Simplex.Messaging.Crypto as C
  import Simplex.Messaging.Encoding.String
  import Simplex.Messaging.Notifications.Protocol
  import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..))
  import Simplex.Messaging.Notifications.Server.Push.APNS
  import Simplex.Messaging.Notifications.Server.Store.Postgres (closeNtfDbStore, newNtfDbStore, withDB')
  import Simplex.Messaging.Notifications.Types (NtfTknAction (..), NtfToken (..))
  import Simplex.Messaging.Parsers (parseAll)
  import Simplex.Messaging.Protocol (ErrorType (AUTH), NetworkError (..), MsgFlags (MsgFlags), NMsgMeta (..), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..))
  import qualified Simplex.Messaging.Protocol as SMP
  import Simplex.Messaging.Server.Env.STM (AStoreType (..), ServerConfig (..))
  import Simplex.Messaging.Transport (ASrvTransport)
  import Simplex.Messaging.Transport.Server (TransportServerConfig (..))
  import System.Process (callCommand)
  import Test.Hspec hiding (fit, it)
  import UnliftIO
  import Util
  #if defined(dbPostgres)
  import Database.PostgreSQL.Simple.SqlQQ (sql)
  #else
  import Database.SQLite.Simple.QQ (sql)
  #endif
  
  notificationTests :: (ASrvTransport, AStoreType) -> Spec
  notificationTests ps@(t, _) = do
    describe "Managing notification tokens" $ do
      it "should register and verify notification token" $
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testNotificationToken apns
      it "should allow repeated registration with the same credentials" $
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testNtfTokenRepeatRegistration apns
      it "should allow the second registration with different credentials and delete the first after verification" $
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testNtfTokenSecondRegistration apns
      it "should verify token after notification server is restarted" $
        withAPNSMockServer $ \apns ->
          testNtfTokenServerRestart t apns
      it "should re-verify token after notification server is restarted" $
        withAPNSMockServer $ \apns ->
          testNtfTokenServerRestartReverify t apns
      it "should re-verify token after notification server is restarted when first request timed-out" $
        withAPNSMockServer $ \apns ->
          testNtfTokenServerRestartReverifyTimeout t apns
      it "should re-register token when notification server is restarted" $
        withAPNSMockServer $ \apns ->
          testNtfTokenServerRestartReregister t apns
      it "should re-register token when notification server is restarted when first request timed-out" $
        withAPNSMockServer $ \apns ->
          testNtfTokenServerRestartReregisterTimeout t apns
      it "should work with multiple configured servers" $
        withAPNSMockServer $ \apns ->
          testNtfTokenMultipleServers t apns
      it "should keep working with active token until replaced" $
        withAPNSMockServer $ \apns ->
          testNtfTokenChangeServers t apns
      it "should re-register token in NTInvalid status after register attempt" $
        withAPNSMockServer $ \apns ->
          testNtfTokenReRegisterInvalid t apns
      it "should re-register token in NTInvalid status after checking token" $
        withAPNSMockServer $ \apns ->
          testNtfTokenReRegisterInvalidOnCheck t apns
    describe "notification server tests" $ do
      it "should pass" $ testRunNTFServerTests t testNtfServer `shouldReturn` Nothing
      let srv1 = testNtfServer {keyHash = "1234"}
      it "should fail with incorrect fingerprint" $ do
        testRunNTFServerTests t srv1 `shouldReturn` Just (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError)
    describe "Managing notification subscriptions" $ do
      describe "should create notification subscription for existing connection" $
        testNtfMatrix ps testNotificationSubscriptionExistingConnection
      describe "should create notification subscription for new connection" $
        testNtfMatrix ps testNotificationSubscriptionNewConnection
      it "should change notifications mode" $
        withSmpServer ps $
          withAPNSMockServer $ \apns ->
            withNtfServer t $ testChangeNotificationsMode apns
      it "should change token" $
        withSmpServer ps $
          withAPNSMockServer $ \apns ->
            withNtfServer t $ testChangeToken apns
    describe "Notifications server store log" $
      it "should save and restore tokens and subscriptions" $
        withAPNSMockServer $ \apns ->
          testNotificationsStoreLog ps apns
    describe "Notifications after SMP server restart" $
      it "should resume subscriptions after SMP server is restarted" $
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testNotificationsSMPRestart ps apns
    describe "Notifications after SMP server restart (batched)" $
      it "should resume batched subscriptions after SMP server is restarted" $
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testNotificationsSMPRestartBatch 50 ps apns
    describe "should switch notifications to the new queue" $
      testServerMatrix2 ps $ \servers ->
        withAPNSMockServer $ \apns ->
          withNtfServer t $ testSwitchNotifications servers apns
    it "should keep sending notifications for old token" $
      withSmpServer ps $
        withAPNSMockServer $ \apns ->
          withNtfServer t $
            testNotificationsOldToken apns
    it "should update server from new token" $
      withSmpServer ps $
        withAPNSMockServer $ \apns ->
          withNtfServerOn t ntfTestPort2 ntfTestDBCfg2 . withNtfServerThreadOn t ntfTestPort ntfTestDBCfg $ \ntf ->
            testNotificationsNewToken apns ntf
    it "should migrate to service subscriptions" $ testMigrateToServiceSubscriptions ps
  
  testNtfMatrix :: HasCallStack => (ASrvTransport, AStoreType) -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> Spec
  testNtfMatrix ps@(_, msType) runTest = do
    describe "next and current" $ do
      it "curr servers; curr clients" $ runNtfTestCfg ps 1 cfg' ntfServerCfg agentCfg agentCfg runTest
      it "curr servers; prev clients" $ runNtfTestCfg ps 1 cfg' ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
      it "prev servers; prev clients" $ runNtfTestCfg ps 1 cfgVPrev' ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
      it "prev servers; curr clients" $ runNtfTestCfg ps 1 cfgVPrev' ntfServerCfgVPrev agentCfg agentCfg runTest
      -- servers can be upgraded in any order
      it "servers: curr SMP, prev NTF; prev clients" $ runNtfTestCfg ps 1 cfg' ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
      it "servers: prev SMP, curr NTF; prev clients" $ runNtfTestCfg ps 1 cfgVPrev' ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
      -- one of two clients can be upgraded
      it "servers: curr SMP, curr NTF; clients: curr/prev" $ runNtfTestCfg ps 1 cfg' ntfServerCfg agentCfg agentCfgVPrevPQ runTest
      it "servers: curr SMP, curr NTF; clients: prev/curr" $ runNtfTestCfg ps 1 cfg' ntfServerCfg agentCfgVPrevPQ agentCfg runTest
    where
      cfg' = cfgMS msType
      cfgVPrev' = cfgVPrev msType
  
  registerNtfToken :: AgentClient -> DeviceToken -> NotificationsMode -> AE NtfTknStatus
  registerNtfToken c = A.registerNtfToken c NRMInteractive
  
  checkNtfToken :: AgentClient -> DeviceToken -> AE NtfTknStatus
  checkNtfToken c = A.checkNtfToken c NRMInteractive
  
  verifyNtfToken :: AgentClient -> DeviceToken -> C.CbNonce -> ByteString -> AE ()
  verifyNtfToken c = A.verifyNtfToken c NRMInteractive
  
  runNtfTestCfg :: HasCallStack => (ASrvTransport, AStoreType) -> AgentMsgId -> AServerConfig -> NtfServerConfig -> AgentConfig -> AgentConfig -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> IO ()
  runNtfTestCfg (t, msType) baseId smpCfg ntfCfg aCfg bCfg runTest = do
    ASSCfg qt mt serverStoreCfg <- pure $ testServerStoreConfig msType
    let smpCfg' = withServerCfg smpCfg $ \cfg_ -> ASrvCfg qt mt cfg_ {serverStoreCfg}
    withSmpServerConfigOn t smpCfg' testPort $ \_ ->
      withAPNSMockServer $ \apns ->
        withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t, False)]} $ \_ ->
          withAgentClientsCfg2 aCfg bCfg $ runTest apns baseId
    threadDelay 100000
  
  testNotificationToken :: APNSMockServer -> IO ()
  testNotificationToken apns = do
    withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
      let tkn = DeviceToken PPApnsTest "abcd"
      NTRegistered <- registerNtfToken a tkn NMPeriodic
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
        getMockNotification apns tkn
      verification <- ntfData .-> "verification"
      nonce <- C.cbNonce <$> ntfData .-> "nonce"
      verifyNtfToken a tkn nonce verification
      NTActive <- checkNtfToken a tkn
      deleteNtfToken a tkn
      -- agent deleted this token
      Left (CMD PROHIBITED _) <- tryE $ checkNtfToken a tkn
      pure ()
  
  (.->) :: J.Value -> J.Key -> ExceptT AgentErrorType IO ByteString
  v .-> key = do
    J.Object o <- pure v
    liftEither . bimap INTERNAL (U.decodeLenient . encodeUtf8) $ JT.parseEither (J..: key) o
  
  -- logCfg :: LogConfig
  -- logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
  
  testNtfTokenRepeatRegistration :: APNSMockServer -> IO ()
  testNtfTokenRepeatRegistration apns = do
    withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
      let tkn = DeviceToken PPApnsTest "abcd"
      NTRegistered <- registerNtfToken a tkn NMPeriodic
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
        getMockNotification apns tkn
      verification <- ntfData .-> "verification"
      nonce <- C.cbNonce <$> ntfData .-> "nonce"
      NTRegistered <- registerNtfToken a tkn NMPeriodic
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
        getMockNotification apns tkn
      _ <- ntfData' .-> "verification"
      _ <- C.cbNonce <$> ntfData' .-> "nonce"
      -- can still use the first verification code, it is the same after decryption
      verifyNtfToken a tkn nonce verification
      NTActive <- checkNtfToken a tkn
      pure ()
  
  testNtfTokenSecondRegistration :: APNSMockServer -> IO ()
  testNtfTokenSecondRegistration apns =
    withAgentClients2 $ \a a' -> runRight_ $ do
      let tkn = DeviceToken PPApnsTest "abcd"
      NTRegistered <- registerNtfToken a tkn NMPeriodic
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
        getMockNotification apns tkn
      verification <- ntfData .-> "verification"
      nonce <- C.cbNonce <$> ntfData .-> "nonce"
      verifyNtfToken a tkn nonce verification
  
      NTRegistered <- registerNtfToken a' tkn NMPeriodic
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
        getMockNotification apns tkn
      verification' <- ntfData' .-> "verification"
      nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
  
      -- at this point the first token is still active
      NTActive <- checkNtfToken a tkn
      -- and the second is not yet verified
      liftIO $ threadDelay 50000
      NTConfirmed <- checkNtfToken a' tkn
      -- now the second token registration is verified
      verifyNtfToken a' tkn nonce' verification'
      -- the first registration is removed
      Left (NTF _ AUTH) <- tryE $ checkNtfToken a tkn
      -- and the second is active
      NTActive <- checkNtfToken a' tkn
      pure ()
  
  testNtfTokenServerRestart :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenServerRestart t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    ntfData <- withAgent 1 agentCfg initAgentServers testDB $ \a ->
      withNtfServer t $ runRight $ do
        NTRegistered <- registerNtfToken a tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
          getMockNotification apns tkn
        pure ntfData
    -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server
    threadDelay 1000000
    withAgent 2 agentCfg initAgentServers testDB $ \a' ->
      -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
      -- so that repeat verification happens without restarting the clients, when notification arrives
      withNtfServer t $ runRight_ $ do
        verification <- ntfData .-> "verification"
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        verifyNtfToken a' tkn nonce verification
        NTActive <- checkNtfToken a' tkn
        pure ()
  
  testNtfTokenServerRestartReverify :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenServerRestartReverify t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    withAgent 1 agentCfg initAgentServers testDB $ \a -> do
      ntfData <- withNtfServer t $ runRight $ do
        NTRegistered <- registerNtfToken a tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
          getMockNotification apns tkn
        pure ntfData
      runRight_ $ do
        verification <- ntfData .-> "verification"
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        Left (BROKER _ (NETWORK _)) <- tryE $ verifyNtfToken a tkn nonce verification
        pure ()
    threadDelay 1500000
    withAgent 2 agentCfg initAgentServers testDB $ \a' ->
      -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
      -- so that repeat verification happens without restarting the clients, when notification arrives
      withNtfServer t $ runRight_ $ do
        NTActive <- registerNtfToken a' tkn NMPeriodic
        NTActive <- checkNtfToken a' tkn
        pure ()
  
  testNtfTokenServerRestartReverifyTimeout :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenServerRestartReverifyTimeout t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do
      (nonce, verification) <- withNtfServer t $ runRight $ do
        NTRegistered <- registerNtfToken a tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
          getMockNotification apns tkn
        verification <- ntfData .-> "verification"
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        verifyNtfToken a tkn nonce verification
        pure (nonce, verification)
      -- this emulates the situation when server verified token but the client did not receive the response
      Just NtfToken {ntfTknStatus = NTActive, ntfTknAction = Just NTACheck, ntfDhSecret = Just dhSecret} <- withTransaction store getSavedNtfToken
      Right code <- pure $ NtfRegCode <$> C.cbDecrypt dhSecret nonce verification
      withTransaction store $ \db ->
        DB.execute
          db
          [sql|
            UPDATE ntf_tokens
            SET tkn_status = ?, tkn_action = ?
            WHERE provider = ? AND device_token = ?
          |]
          (NTConfirmed, Just (NTAVerify code), PPApnsTest, "abcd" :: ByteString)
      Just NtfToken {ntfTknStatus = NTConfirmed, ntfTknAction = Just (NTAVerify _)} <- withTransaction store getSavedNtfToken
      pure ()
    threadDelay 1500000
    withAgent 2 agentCfg initAgentServers testDB $ \a' ->
      -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
      -- so that repeat verification happens without restarting the clients, when notification arrives
      withNtfServer t $ runRight_ $ do
        NTActive <- registerNtfToken a' tkn NMPeriodic
        NTActive <- checkNtfToken a' tkn
        pure ()
  
  testNtfTokenServerRestartReregister :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenServerRestartReregister t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    withAgent 1 agentCfg initAgentServers testDB $ \a ->
      withNtfServer t $ runRight $ do
        NTRegistered <- registerNtfToken a tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}} <-
          getMockNotification apns tkn
        pure ()
    -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server
    threadDelay 1000000
    withAgent 2 agentCfg initAgentServers testDB $ \a' ->
      -- server stopped before token is verified, and client might have lost verification notification.
      -- so that repeat registration happens when client is restarted.
      withNtfServer t $ runRight_ $ do
        NTRegistered <- registerNtfToken a' tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
          getMockNotification apns tkn
        verification <- ntfData .-> "verification"
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        verifyNtfToken a' tkn nonce verification
        NTActive <- checkNtfToken a' tkn
        pure ()
  
  testNtfTokenServerRestartReregisterTimeout :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenServerRestartReregisterTimeout t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do
      withNtfServer t $ runRight $ do
        NTRegistered <- registerNtfToken a tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}} <-
          getMockNotification apns tkn
        pure ()
      -- this emulates the situation when server registered token but the client did not receive the response
      withTransaction store $ \db ->
        DB.execute
          db
          [sql|
            UPDATE ntf_tokens
            SET tkn_id = NULL, tkn_dh_secret = NULL, tkn_status = ?, tkn_action = ?
            WHERE provider = ? AND device_token = ?
          |]
          (NTNew, Just NTARegister, PPApnsTest, "abcd" :: ByteString)
      Just NtfToken {ntfTokenId = Nothing, ntfTknStatus = NTNew, ntfTknAction = Just NTARegister} <- withTransaction store getSavedNtfToken
      pure ()
    threadDelay 1000000
    withAgent 2 agentCfg initAgentServers testDB $ \a' ->
      -- server stopped before token is verified, and client might have lost verification notification.
      -- so that repeat registration happens when client is restarted.
      withNtfServer t $ runRight_ $ do
        NTRegistered <- registerNtfToken a' tkn NMPeriodic
        APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
          getMockNotification apns tkn
        verification <- ntfData .-> "verification"
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        verifyNtfToken a' tkn nonce verification
        NTActive <- checkNtfToken a' tkn
        pure ()
  
  getTestNtfTokenPort :: AgentClient -> AE String
  getTestNtfTokenPort a =
    ExceptT (runExceptT (withStore' a getSavedNtfToken) `runReaderT` agentEnv a) >>= \case
      Just NtfToken {ntfServer = ProtocolServer {port}} -> pure port
      Nothing -> error "no active NtfToken"
  
  testNtfTokenMultipleServers :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenMultipleServers t apns = do
    let tkn = DeviceToken PPApnsTest "abcd"
    withAgent 1 agentCfg initAgentServers2 testDB $ \a ->
      withNtfServerThreadOn t ntfTestPort ntfTestDBCfg $ \ntf ->
        withNtfServerThreadOn t ntfTestPort2 ntfTestDBCfg2 $ \ntf2 -> runRight_ $ do
          -- register a new token, the agent picks a server and stores its choice
          NTRegistered <- registerNtfToken a tkn NMPeriodic
          APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
            getMockNotification apns tkn
          verification <- ntfData .-> "verification"
          nonce <- C.cbNonce <$> ntfData .-> "nonce"
          verifyNtfToken a tkn nonce verification
          NTActive <- checkNtfToken a tkn
          -- shut down the "other" server
          port <- getTestNtfTokenPort a
          liftIO . killThread $ if port == ntfTestPort then ntf2 else ntf
          -- still works
          NTActive <- checkNtfToken a tkn
          liftIO . killThread $ if port == ntfTestPort then ntf else ntf2
          -- negative test, the correct server is now gone
          Left _ <- tryError (checkNtfToken a tkn)
          pure ()
  
  testNtfTokenChangeServers :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenChangeServers t apns =
    withNtfServerThreadOn t ntfTestPort ntfTestDBCfg $ \ntf -> do
      tkn1 <- withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight $ do
        tkn <- registerTestToken a "abcd" NMInstant apns
        NTActive <- checkNtfToken a tkn
        liftIO $ setNtfServers a [testNtfServer2]
        NTActive <- checkNtfToken a tkn -- still works on old server
        pure tkn
  
      threadDelay 1000000
  
      withAgent 2 agentCfg initAgentServers testDB $ \a -> do
        runRight_ $ do
          getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
          NTActive <- checkNtfToken a tkn1
          liftIO $ setNtfServers a [testNtfServer2] -- just change configured server list
          getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
          -- trigger token replace
          tkn2 <- registerTestToken a "xyzw" NMInstant apns
          getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
          deleteNtfToken a tkn2 -- force server switch
          Left BROKER {brokerErr = (NETWORK _)} <- tryError $ registerTestToken a "qwer" NMInstant apns -- ok, it's down for now
          getTestNtfTokenPort a >>= \port2 -> liftIO $ port2 `shouldBe` ntfTestPort2 -- but the token got updated
        killThread ntf
        withNtfServerOn t ntfTestPort2 ntfTestDBCfg2 $ runRight_ $ do
          liftIO $ threadDelay 1000000 -- for notification server to reconnect
          tkn <- registerTestToken a "qwer" NMInstant apns
          checkNtfToken a tkn >>= \r -> liftIO $ r `shouldBe` NTActive
  
  testNtfTokenReRegisterInvalid :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenReRegisterInvalid t apns = do
    tkn <- withNtfServer t $ do
      withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight $ do
        tkn <- registerTestToken a "abcd" NMInstant apns
        NTActive <- checkNtfToken a tkn
        pure tkn
  
    threadDelay 250000
    -- start server to compact
    withNtfServer t $ pure ()
  
    threadDelay 250000
    st <- newNtfDbStore ntfTestDBCfg
    Right 1 <- withDB' "test" st $ \db -> PSQL.execute db "UPDATE tokens SET status = ? WHERE status = ?" (NTInvalid Nothing, NTActive)
    closeNtfDbStore st
  
    threadDelay 250000
    withNtfServer t $ do
      withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
        NTInvalid Nothing <- registerNtfToken a tkn NMInstant
        tkn1 <- registerTestToken a "abcd" NMInstant apns
        NTActive <- checkNtfToken a tkn1
        pure ()
  
  testNtfTokenReRegisterInvalidOnCheck :: ASrvTransport -> APNSMockServer -> IO ()
  testNtfTokenReRegisterInvalidOnCheck t apns = do
    tkn <- withNtfServer t $ do
      withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight $ do
        tkn <- registerTestToken a "abcd" NMInstant apns
        NTActive <- checkNtfToken a tkn
        pure tkn
  
    threadDelay 250000
    -- start server to compact
    withNtfServer t $ pure ()
  
    threadDelay 250000
    st <- newNtfDbStore ntfTestDBCfg
    Right 1 <- withDB' "test" st $ \db -> PSQL.execute db "UPDATE tokens SET status = ? WHERE status = ?" (NTInvalid Nothing, NTActive)
    closeNtfDbStore st
  
    threadDelay 250000
    withNtfServer t $ do
      withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
        NTInvalid Nothing <- checkNtfToken a tkn
        tkn1 <- registerTestToken a "abcd" NMInstant apns
        NTActive <- checkNtfToken a tkn1
        pure ()
  
  testRunNTFServerTests :: ASrvTransport -> NtfServer -> IO (Maybe ProtocolTestFailure)
  testRunNTFServerTests t srv =
    withNtfServer t $
      withAgent 1 agentCfg initAgentServers testDB $ \a ->
        testProtocolServer a NRMInteractive 1 $ ProtoServerWithAuth srv Nothing
  
  testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()
  testNotificationSubscriptionExistingConnection apns baseId alice@AgentClient {agentEnv = Env {config = aliceCfg, store}} bob = do
    (bobId, aliceId, nonce, message) <- runRight $ do
      -- establish connection
      (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
      (aliceId, _sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
      ("", _, CONF confId _ "bob's connInfo") <- get alice
      allowConnection alice bobId confId "alice's connInfo"
      get bob ##> ("", aliceId, INFO "alice's connInfo")
      get alice ##> ("", bobId, CON)
      get bob ##> ("", aliceId, CON)
      -- register notification token
      let tkn = DeviceToken PPApnsTest "abcd"
      NTRegistered <- registerNtfToken alice tkn NMInstant
      APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
        getMockNotification apns tkn
      verification <- ntfData .-> "verification"
      vNonce <- C.cbNonce <$> ntfData .-> "nonce"
      verifyNtfToken alice tkn vNonce verification
      NTActive <- checkNtfToken alice tkn
      -- send message
      1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
      get bob ##> ("", aliceId, SENT $ baseId + 1)
      -- notification
      (nonce, message) <- messageNotification apns tkn
      pure (bobId, aliceId, nonce, message)
  
    Right [NotificationInfo {ntfConnId = cId, ntfMsgMeta = Just NMsgMeta {msgTs}}] <- runExceptT $ getNotificationConns alice nonce message
    cId `shouldBe` bobId
    -- alice client already has subscription for the connection,
    [Left (CMD PROHIBITED _)] <- getConnectionMessages alice [ConnMsgReq cId 1 $ Just $ systemToUTCTime msgTs]
  
    threadDelay 1000000
    suspendAgent alice 0
    closeDBStore store
    threadDelay 1000000 >> callCommand "sync" >> threadDelay 1000000
  
    -- aliceNtf client doesn't have subscription and is allowed to get notification message
    withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> do
      (Right (Just SMPMsgMeta {msgFlags = MsgFlags True})) :| _ <- getConnectionMessages aliceNtf [ConnMsgReq cId 1 $ Just $ systemToUTCTime msgTs]
      pure ()
  
    threadDelay 1000000 >> callCommand "sync" >> threadDelay 1000000
    reopenDBStore store
    foregroundAgent alice
    threadDelay 500000
  
    runRight_ $ do
      get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 1) Nothing
      -- delete notification subscription
      toggleConnectionNtfs alice bobId False
      liftIO $ threadDelay 500000
      -- send message
      2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
      get bob ##> ("", aliceId, SENT $ baseId + 2)
      -- no notifications should follow
      noNotification alice apns
    where
      msgId = subtract baseId
  
  testNotificationSubscriptionNewConnection :: HasCallStack => APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()
  testNotificationSubscriptionNewConnection apns baseId alice bob =
    runRight_ $ do
      -- alice registers notification token
      DeviceToken {} <- registerTestToken alice "abcd" NMInstant apns
      -- bob registers notification token
      DeviceToken {} <- registerTestToken bob "bcde" NMInstant apns
      -- establish connection
      liftIO $ threadDelay 50000
      (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
      liftIO $ threadDelay 1000000
      (aliceId, _sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
      liftIO $ threadDelay 750000
      void $ messageNotificationData alice apns
      ("", _, CONF confId _ "bob's connInfo") <- get alice
      liftIO $ threadDelay 500000
      allowConnection alice bobId confId "alice's connInfo"
      void $ messageNotificationData bob apns
      get bob ##> ("", aliceId, INFO "alice's connInfo")
      when (baseId == 3) $ void $ messageNotificationData alice apns
      get alice ##> ("", bobId, CON)
      when (baseId == 3) $ void $ messageNotificationData bob apns
      get bob ##> ("", aliceId, CON)
      -- bob sends message
      1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
      get bob ##> ("", aliceId, SENT $ baseId + 1)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 1) Nothing
      -- alice sends message
      2 <- msgId <$> sendMessage alice bobId (SMP.MsgFlags True) "hey there"
      get alice ##> ("", bobId, SENT $ baseId + 2)
      void $ messageNotificationData bob apns
      get bob =##> \case ("", c, Msg "hey there") -> c == aliceId; _ -> False
      ackMessage bob aliceId (baseId + 2) Nothing
      -- no unexpected notifications should follow
      noNotifications apns
    where
      msgId = subtract baseId
  
  registerTestToken :: AgentClient -> ByteString -> NotificationsMode -> APNSMockServer -> ExceptT AgentErrorType IO DeviceToken
  registerTestToken a token mode apns = do
    let tkn = DeviceToken PPApnsTest token
    NTRegistered <- registerNtfToken a tkn mode
    Just APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
      timeout 1000000 $ getMockNotification apns tkn
    verification' <- ntfData' .-> "verification"
    nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
    verifyNtfToken a tkn nonce' verification'
    NTActive <- checkNtfToken a tkn
    pure tkn
  
  testChangeNotificationsMode :: HasCallStack => APNSMockServer -> IO ()
  testChangeNotificationsMode apns =
    withAgentClients2 $ \alice bob -> runRight_ $ do
      -- establish connection
      (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
      (aliceId, sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
      liftIO $ sqSecured `shouldBe` True
      ("", _, CONF confId _ "bob's connInfo") <- get alice
      allowConnection alice bobId confId "alice's connInfo"
      get bob ##> ("", aliceId, INFO "alice's connInfo")
      get alice ##> ("", bobId, CON)
      get bob ##> ("", aliceId, CON)
      -- register notification token, set mode to NMInstant
      tkn <- registerTestToken alice "abcd" NMInstant apns
      -- send message, receive notification
      liftIO $ threadDelay 500000
      1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
      get bob ##> ("", aliceId, SENT $ baseId + 1)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 1) Nothing
      -- set mode to NMPeriodic
      NTActive <- registerNtfToken alice tkn NMPeriodic
      -- send message, no notification
      liftIO $ threadDelay 750000
      2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
      get bob ##> ("", aliceId, SENT $ baseId + 2)
      noNotification alice apns
      get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 2) Nothing
      -- set mode to NMInstant
      NTActive <- registerNtfToken alice tkn NMInstant
      -- send message, receive notification
      liftIO $ threadDelay 500000
      3 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello there"
      get bob ##> ("", aliceId, SENT $ baseId + 3)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello there") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 3) Nothing
      -- turn off notifications
      deleteNtfToken alice tkn
      -- send message, no notification
      liftIO $ threadDelay 500000
      4 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "why hello there"
      get bob ##> ("", aliceId, SENT $ baseId + 4)
      noNotifications apns
      get alice =##> \case ("", c, Msg "why hello there") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 4) Nothing
      -- turn on notifications, set mode to NMInstant
      void $ registerTestToken alice "abcd" NMInstant apns
      -- send message, receive notification
      liftIO $ threadDelay 500000
      5 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hey"
      get bob ##> ("", aliceId, SENT $ baseId + 5)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hey") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 5) Nothing
      -- no notifications should follow
      noNotification alice apns
    where
      baseId = 1
      msgId = subtract baseId
  
  testChangeToken :: APNSMockServer -> IO ()
  testChangeToken apns = withAgent 1 agentCfg initAgentServers testDB2 $ \bob -> do
    (aliceId, bobId) <- withAgent 2 agentCfg initAgentServers testDB $ \alice -> runRight $ do
      -- establish connection
      (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
      (aliceId, sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
      liftIO $ sqSecured `shouldBe` True
      ("", _, CONF confId _ "bob's connInfo") <- get alice
      allowConnection alice bobId confId "alice's connInfo"
      get bob ##> ("", aliceId, INFO "alice's connInfo")
      get alice ##> ("", bobId, CON)
      get bob ##> ("", aliceId, CON)
      -- register notification token, set mode to NMInstant
      void $ registerTestToken alice "abcd" NMInstant apns
      -- send message, receive notification
      liftIO $ threadDelay 500000
      1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
      get bob ##> ("", aliceId, SENT $ baseId + 1)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
      ackMessage alice bobId (baseId + 1) Nothing
      pure (aliceId, bobId)
  
    withAgent 3 agentCfg initAgentServers testDB $ \alice1 -> runRight_ $ do
      void $ subscribeConnection alice1 bobId
      -- change notification token
      void $ registerTestToken alice1 "bcde" NMInstant apns
      -- send message, receive notification
      liftIO $ threadDelay 500000
      2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello there"
      get bob ##> ("", aliceId, SENT $ baseId + 2)
      void $ messageNotificationData alice1 apns
      get alice1 =##> \case ("", c, Msg "hello there") -> c == bobId; _ -> False
      ackMessage alice1 bobId (baseId + 2) Nothing
      -- no notifications should follow
      noNotification alice1 apns
    where
      baseId = 1
      msgId = subtract baseId
  
  testNotificationsStoreLog :: (ASrvTransport, AStoreType) -> APNSMockServer -> IO ()
  testNotificationsStoreLog ps@(t, _) apns = withAgentClients2 $ \alice bob -> do
    withSmpServerStoreMsgLogOn ps testPort $ \_ -> do
      (aliceId, bobId) <- withNtfServer t $ runRight $ do
        (aliceId, bobId) <- makeConnection alice bob
        _ <- registerTestToken alice "abcd" NMInstant apns
        liftIO $ threadDelay 250000
        2 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello"
        get bob ##> ("", aliceId, SENT 2)
        void $ messageNotificationData alice apns
        get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
        ackMessage alice bobId 2 Nothing
        pure (aliceId, bobId)
  
      liftIO $ threadDelay 250000
  
      withNtfServer t $ runRight_ $ do
        liftIO $ threadDelay 250000
        3 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
        get bob ##> ("", aliceId, SENT 3)
        void $ messageNotificationData alice apns
        get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
        ackMessage alice bobId 3 Nothing
  
      runRight_ $ do
        4 <- sendMessage bob aliceId (SMP.MsgFlags True) "message 4"
        get bob ##> ("", aliceId, SENT 4)
        get alice =##> \case ("", c, Msg "message 4") -> c == bobId; _ -> False
        ackMessage alice bobId 4 Nothing
        noNotifications apns
  
    withSmpServerStoreMsgLogOn ps testPort $ \_ ->
      withNtfServer t $ runRight_ $ do
        void $ messageNotificationData alice apns
  
  testNotificationsSMPRestart :: (ASrvTransport, AStoreType) -> APNSMockServer -> IO ()
  testNotificationsSMPRestart ps apns = withAgentClients2 $ \alice bob -> do
    (aliceId, bobId) <- withSmpServerStoreLogOn ps testPort $ \threadId -> runRight $ do
      (aliceId, bobId) <- makeConnection alice bob
      _ <- registerTestToken alice "abcd" NMInstant apns
      liftIO $ threadDelay 250000
      2 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello"
      get bob ##> ("", aliceId, SENT 2)
      void $ messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
      ackMessage alice bobId 2 Nothing
      liftIO $ killThread threadId
      pure (aliceId, bobId)
  
    runRight_ @AgentErrorType $ do
      nGet alice =##> \case ("", "", DOWN _ [c]) -> c == bobId; _ -> False
      nGet bob =##> \case ("", "", DOWN _ [c]) -> c == aliceId; _ -> False
  
    withSmpServerStoreLogOn ps testPort $ \threadId -> runRight_ $ do
      nGet alice =##> \case ("", "", UP _ [c]) -> c == bobId; _ -> False
      nGet bob =##> \case ("", "", UP _ [c]) -> c == aliceId; _ -> False
      liftIO $ threadDelay 1000000
      3 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
      get bob ##> ("", aliceId, SENT 3)
      _ <- messageNotificationData alice apns
      get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
      liftIO $ killThread threadId
  
  testNotificationsSMPRestartBatch :: Int -> (ASrvTransport, AStoreType) -> APNSMockServer -> IO ()
  testNotificationsSMPRestartBatch n ps@(t, ASType qsType _) apns =
    withAgentClientsCfgServers2 agentCfg agentCfg initAgentServers2 $ \a b -> do
      threadDelay 1000000
      conns <- runServers $ do
        conns <- replicateM (n :: Int) $ makeConnection a b
        _ <- registerTestToken a "abcd" NMInstant apns
        liftIO $ threadDelay 5000000
        forM_ conns $ \(aliceId, bobId) -> do
          msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello"
          get b ##> ("", aliceId, SENT msgId)
          void $ messageNotificationData a apns
          get a =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
          ackMessage a bobId msgId Nothing
        pure conns
  
      runRight_ @AgentErrorType $ do
        ("", "", DOWN _ bcs1) <- nGet a
        ("", "", DOWN _ bcs2) <- nGet a
        liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
        ("", "", DOWN _ acs1) <- nGet b
        ("", "", DOWN _ acs2) <- nGet b
        liftIO $ length (acs1 <> acs2) `shouldBe` length conns
  
      runServers $ do
        ("", "", UP _ bcs1) <- nGet a
        ("", "", UP _ bcs2) <- nGet a
        liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
        ("", "", UP _ acs1) <- nGet b
        ("", "", UP _ acs2) <- nGet b
        liftIO $ length (acs1 <> acs2) `shouldBe` length conns
        liftIO $ threadDelay 1500000
        forM_ conns $ \(aliceId, bobId) -> do
          msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello again"
          get b ##> ("", aliceId, SENT msgId)
          _ <- messageNotificationData a apns
          get a =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
    where
      runServers :: ExceptT AgentErrorType IO a -> IO a
      runServers a = do
        withSmpServerStoreLogOn ps testPort $ \t1 -> do
          res <- withSmpServerConfigOn t (cfgJ2QS qsType) testPort2 $ \t2 ->
            runRight a `finally` killThread t2
          killThread t1
          pure res
  
  testSwitchNotifications :: InitialAgentServers -> APNSMockServer -> IO ()
  testSwitchNotifications servers apns =
    withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do
      (aId, bId) <- makeConnection a b
      exchangeGreetings a bId b aId
      _ <- registerTestToken a "abcd" NMInstant apns
      liftIO $ threadDelay 250000
      let testMessage msg = do
            msgId <- sendMessage b aId (SMP.MsgFlags True) msg
            get b ##> ("", aId, SENT msgId)
            void $ messageNotificationData a apns
            get a =##> \case ("", c, Msg msg') -> c == bId && msg == msg'; _ -> False
            ackMessage a bId msgId Nothing
      testMessage "hello"
      _ <- switchConnectionAsync a "" bId
      switchComplete a bId b aId
      liftIO $ threadDelay 500000
      testMessage "hello again"
  
  testNotificationsOldToken :: APNSMockServer -> IO ()
  testNotificationsOldToken apns =
    withAgentClients3 $ \a b c -> runRight_ $ do
      (abId, baId) <- makeConnection a b
      let testMessageAB = testMessage_ apns a abId b baId
      _ <- registerTestToken a "abcd" NMInstant apns
      liftIO $ threadDelay 250000
      testMessageAB "hello"
      -- change server
      liftIO $ setNtfServers a [testNtfServer2] -- server 2 isn't running now, don't use
      -- replacing token keeps server
      _ <- registerTestToken a "xyzw" NMInstant apns
      getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
      testMessageAB "still there"
      -- new connections keep server
      (acId, caId) <- makeConnection a c
      let testMessageAC = testMessage_ apns a acId c caId
      testMessageAC "greetings"
  
  testNotificationsNewToken :: APNSMockServer -> ThreadId -> IO ()
  testNotificationsNewToken apns oldNtf =
    withAgentClients3 $ \a b c -> runRight_ $ do
      (abId, baId) <- makeConnection a b
      let testMessageAB = testMessage_ apns a abId b baId
      tkn <- registerTestToken a "abcd" NMInstant apns
      getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
      liftIO $ threadDelay 250000
      testMessageAB "hello"
      -- switch
      liftIO $ setNtfServers a [testNtfServer2]
      deleteNtfToken a tkn
      _ <- registerTestToken a "abcd" NMInstant apns
      getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort2
      liftIO $ threadDelay 250000
      liftIO $ killThread oldNtf
      -- -- back to work
      testMessageAB "hello again"
      (acId, caId) <- makeConnection a c
      let testMessageAC = testMessage_ apns a acId c caId
      testMessageAC "greetings"
  
  testMigrateToServiceSubscriptions :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
  testMigrateToServiceSubscriptions ps@(t, msType) = withAgentClients2 $ \a b -> do
    (c1, c2, c3) <- withSmpServerConfigOn t cfgNoService testPort $ \_ -> do
      (c1, c2) <- withAPNSMockServer $ \apns -> do
        withNtfServerCfg ntfCfgNoService $ \_ -> runRight $ do
          _tkn <- registerTestToken a "abcd" NMInstant apns
          -- create 2 connections with ntfs, test delivery
          c1 <- testConnectMsg apns a b "hello"
          c2 <- testConnectMsg apns a b "hello too"
          pure (c1, c2)
      liftIO $ threadDelay 250000
      fmap (c1,c2,) $ withAPNSMockServer $ \apns ->
        withNtfServer t $ runRight $ do
          liftIO $ threadDelay 250000
          testSendMsg apns a b c1 "hello 1"
          testSendMsg apns a b c2 "hello 2"
          testConnectMsg apns a b "hello 3"
    serverDOWN a b 3
  
    -- this session creates association of subscriptions with service
    c4 <- withAPNSMockServer $ \apns -> withSmpServer ps $ withNtfServer t $ do
      serverUP a b 3
      runRight $ do
        liftIO $ threadDelay 250000
        testSendMsg apns a b c1 "hey 1"
        testSendMsg apns a b c2 "hey 2"
        testSendMsg apns a b c3 "hey 3"
        testConnectMsg apns a b "hey 4"
    serverDOWN a b 4
  
    -- this session uses service to subscribe
    c5 <- withAPNSMockServer $ \apns -> withSmpServer ps $ withNtfServer t $ do
      serverUP a b 4
      runRight $ do
        liftIO $ threadDelay 250000
        testSendMsg apns a b c1 "hi 1"
        testSendMsg apns a b c2 "hi 2"
        testSendMsg apns a b c3 "hi 3"
        testSendMsg apns a b c4 "hi 4"
        testConnectMsg apns a b "hi 5"
    serverDOWN a b 5
  
    -- Ntf server does not use server, subscriptions downgrade
    c6 <- withAPNSMockServer $ \apns -> withSmpServer ps $ withNtfServerCfg ntfCfgNoService $ \_ -> do
      serverUP a b 5
      runRight $ do
        testSendMsg apns a b c1 "msg 1"
        testSendMsg apns a b c2 "msg 2"
        testSendMsg apns a b c3 "msg 3"
        testSendMsg apns a b c4 "msg 4"
        testSendMsg apns a b c5 "msg 5"
        testConnectMsg apns a b "msg 6"
    serverDOWN a b 6
  
    withAPNSMockServer $ \apns -> withSmpServerConfigOn t cfgNoService testPort $ \_ -> withNtfServerCfg ntfCfgNoService $ \_ -> do
      serverUP a b 6
      runRight_ $ do
        testSendMsg apns a b c1 "1"
        testSendMsg apns a b c2 "2"
        testSendMsg apns a b c3 "3"
        testSendMsg apns a b c4 "4"
        testSendMsg apns a b c5 "5"
        testSendMsg apns a b c6 "6"
        void $ testConnectMsg apns a b "7"
    serverDOWN a b 7
    where
      testConnectMsg apns a b msg = do
        conn <- makeConnection a b
        liftIO $ threadDelay 250000
        testSendMsg apns a b conn msg
        pure conn
      testSendMsg :: HasCallStack => APNSMockServer -> AgentClient -> AgentClient -> (ConnId, ConnId) -> SMP.MsgBody -> ExceptT AgentErrorType IO ()
      testSendMsg apns a b (abId, baId) = testMessage_ apns a abId b baId
      serverDOWN a b n = do
        ("", "", DOWN _ cs) <- nGet a
        ("", "", DOWN _ cs') <- nGet b
        length cs `shouldBe` n
        length cs' `shouldBe` n
      serverUP a b n = do
        ("", "", UP _ cs) <- nGet a
        ("", "", UP _ cs') <- nGet b
        length cs `shouldBe` n
        length cs' `shouldBe` n
      cfgNoService = updateCfg (cfgMS msType) $ \(cfg' :: ServerConfig s) ->
        let ServerConfig {transportConfig} = cfg'
         in cfg' {transportConfig = transportConfig {askClientCert = False}} :: ServerConfig s
      ntfCfgNoService = ntfServerCfg {useServiceCreds = False, transports = [(ntfTestPort, t, False)]}
  
  testMessage_ :: HasCallStack => APNSMockServer -> AgentClient -> ConnId -> AgentClient -> ConnId -> SMP.MsgBody -> ExceptT AgentErrorType IO ()
  testMessage_ apns a aId b bId msg = do
    msgId <- sendMessage b aId (SMP.MsgFlags True) msg
    get b ##> ("", aId, SENT msgId)
    void $ messageNotificationData a apns
    get a =##> \case ("", c, Msg msg') -> c == bId && msg == msg'; _ -> False
    ackMessage a bId msgId Nothing
  
  messageNotification :: HasCallStack => APNSMockServer -> DeviceToken -> ExceptT AgentErrorType IO (C.CbNonce, ByteString)
  messageNotification apns tkn = do
    500000 `timeout` getMockNotification apns tkn >>= \case
      Nothing -> error "no notification"
      Just APNSMockRequest {notification = APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData}} -> do
        nonce <- C.cbNonce <$> ntfData .-> "nonce"
        message <- ntfData .-> "message"
        pure (nonce, message)
      _ -> error "bad notification"
  
  messageNotificationData :: HasCallStack => AgentClient -> APNSMockServer -> ExceptT AgentErrorType IO PNMessageData
  messageNotificationData c apns = do
    NtfToken {deviceToken, ntfDhSecret = Just dhSecret} <- getNtfTokenData c
    (nonce, message) <- messageNotification apns deviceToken
    Right pnMsgs <- liftEither . first INTERNAL $ Right . parseAll pnMessagesP =<< first show (C.cbDecrypt dhSecret nonce message)
    pure $ L.last pnMsgs
  
  noNotification :: AgentClient -> APNSMockServer -> ExceptT AgentErrorType IO ()
  noNotification c apns = do
    NtfToken {deviceToken} <- getNtfTokenData c
    500000 `timeout` getMockNotification apns deviceToken >>= \case
      Nothing -> pure ()
      _ -> error "unexpected notification"
  
  noNotifications :: APNSMockServer -> ExceptT AgentErrorType IO ()
  noNotifications apns = do
    500000 `timeout` getAnyMockNotification apns >>= \case
      Nothing -> pure ()
      _ -> error "unexpected notification"
  