content: {-# LANGUAGE CPP #-}
  {-# LANGUAGE DuplicateRecordFields #-}
  {-# LANGUAGE NamedFieldPuns #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE PostfixOperators #-}
  
  module Bots.DirectoryTests where
  
  import ChatClient
  import ChatTests.DBUtils
  import ChatTests.Utils
  import Control.Concurrent (forkIO, killThread, threadDelay)
  import Control.Exception (finally)
  import Control.Monad (forM_, when)
  import qualified Data.Text as T
  import Directory.Captcha
  import Directory.Options
  import Directory.Service
  import Directory.Store
  import GHC.IO.Handle (hClose)
  import Simplex.Chat.Bot.KnownContacts
  import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
  import Simplex.Chat.Core
  import qualified Simplex.Chat.Markdown as MD
  import Simplex.Chat.Options (CoreChatOpts (..))
  import Simplex.Chat.Options.DB
  import Simplex.Chat.Types (ChatPeerType (..), Profile (..))
  import Simplex.Chat.Types.Shared (GroupMemberRole (..))
  import System.FilePath ((</>))
  import Test.Hspec hiding (it)
  
  directoryServiceTests :: SpecWith TestParams
  directoryServiceTests = do
    it "should register group" testDirectoryService
    it "should suspend and resume group, send message to owner" testSuspendResume
    it "should delete group registration" testDeleteGroup
    it "admin should delete group registration" testDeleteGroupAdmin
    it "should change initial member role" testSetRole
    it "should join found group via link" testJoinGroup
    it "should support group names with spaces" testGroupNameWithSpaces
    it "should return more groups in search, all and recent groups" testSearchGroups
    it "should invite to owners' group if specified" testInviteToOwnersGroup
    describe "de-listing the group" $ do
      it "should de-list if owner leaves the group" testDelistedOwnerLeaves
      it "should de-list if owner is removed from the group" testDelistedOwnerRemoved
      it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves
      it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved
      it "should de-list if service is removed from the group" testDelistedServiceRemoved
      it "should de-list if group is deleted" testDelistedGroupDeleted
      it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges
      it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged
      it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles
      it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles
    describe "should require re-approval if profile is changed by" $ do
      it "the registration owner" testRegOwnerChangedProfile
      it "another owner" testAnotherOwnerChangedProfile
      it "another owner not connected to directory" testNotConnectedOwnerChangedProfile
    describe "should require profile update if group link is removed by " $ do
      it "the registration owner" testRegOwnerRemovedLink
      it "another owner" testAnotherOwnerRemovedLink
      it "another owner not connected to directory" testNotConnectedOwnerRemovedLink
    describe "duplicate groups (same display name and full name)" $ do
      it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation
      it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration
      it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation
      it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated
      it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval
    describe "list groups" $ do
      it "should list user's groups" testListUserGroups
    describe "member admission" $ do
      it "should ask member to pass captcha screen" testCapthaScreening
    describe "store log" $ do
      it "should restore directory service state" testRestoreDirectory
    describe "captcha" $ do
      it "should accept some incorrect spellings" testCaptcha
  
  directoryProfile :: Profile
  directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing}
  
  mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> DirectoryOpts
  mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup =
    DirectoryOpts
      { coreOptions =
          testCoreOpts
            { dbOptions =
                (dbOptions testCoreOpts)
  #if defined(dbPostgres)
                  {dbSchemaPrefix = "client_" <> serviceDbPrefix}
  #else
                  {dbFilePrefix = ps </> serviceDbPrefix}
  #endif
  
            },
        adminUsers = [],
        superUsers,
        ownersGroup,
        blockedFragmentsFile = Nothing,
        blockedWordsFile = Nothing,
        blockedExtensionRules = Nothing,
        nameSpellingFile = Nothing,
        profileNameLimit = maxBound,
        captchaGenerator = Nothing,
        directoryLog = Just $ ps </> "directory_service.log",
        serviceName = "SimpleX Directory",
        runCLI = False,
        searchResults = 3,
        testing = True
      }
  
  serviceDbPrefix :: FilePath
  serviceDbPrefix = "directory_service"
  
  viewName :: String -> String
  viewName = T.unpack . MD.viewName . T.pack
  
  testDirectoryService :: HasCallStack => TestParams -> IO ()
  testDirectoryService ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          bob #> "@'SimpleX Directory' privacy"
          bob <# "'SimpleX Directory'> > privacy"
          bob <## "      No groups found"
          -- putStrLn "*** create a group"
          bob ##> "/g PSA Privacy, Security & Anonymity"
          bob <## "group #PSA (Privacy, Security & Anonymity) is created"
          bob <## "to add members use /a PSA <name> or /create link #PSA"
          bob ##> "/a PSA 'SimpleX Directory' member"
          bob <## "invitation to join the group #PSA sent to 'SimpleX Directory'"
          bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
          bob ##> "/mr PSA 'SimpleX Directory' admin"
          -- putStrLn "*** discover service joins group and creates the link for profile"
          bob <## "#PSA: you changed the role of 'SimpleX Directory' to admin"
          bob <# "'SimpleX Directory'> Joining the group PSA…"
          bob <## "#PSA: 'SimpleX Directory' joined the group"
          bob <# "'SimpleX Directory'> Joined the group PSA, creating the link…"
          bob <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
          bob <## ""
          bob <## "Please add it to the group welcome message."
          bob <## "For example, add:"
          welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
          -- putStrLn "*** update profile without link"
          updateGroupProfile bob "Welcome!"
          bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message."
          (superUser </)
          -- putStrLn "*** update profile so that it has link"
          updateGroupProfile bob welcomeWithLink
          bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
          bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          approvalRequested superUser welcomeWithLink (1 :: Int)
          -- putStrLn "*** update profile so that it still has link"
          let welcomeWithLink' = "Welcome! " <> welcomeWithLink
          updateGroupProfile bob welcomeWithLink'
          bob <# "'SimpleX Directory'> The group ID 1 (PSA) is updated!"
          bob <## "It is hidden from the directory until approved."
          superUser <# "'SimpleX Directory'> The group ID 1 (PSA) is updated."
          approvalRequested superUser welcomeWithLink' (2 :: Int)
          -- putStrLn "*** try approving with the old registration code"
          bob #> "@'SimpleX Directory' /approve 1:PSA 1"
          bob <# "'SimpleX Directory'> > /approve 1:PSA 1"
          bob <## "      You are not allowed to use this command"
          superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
          superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
          superUser <## "      Incorrect approval code"
          -- putStrLn "*** update profile so that it has no link"
          updateGroupProfile bob "Welcome!"
          bob <# "'SimpleX Directory'> The group link for ID 1 (PSA) is removed from the welcome message."
          bob <## ""
          bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
          superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (PSA), de-listed."
          superUser #> "@'SimpleX Directory' /approve 1:PSA 2"
          superUser <# "'SimpleX Directory'> > /approve 1:PSA 2"
          superUser <## "      Error: the group ID 1 (PSA) is not pending approval."
          -- putStrLn "*** update profile so that it has link again"
          updateGroupProfile bob welcomeWithLink'
          bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
          bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          approvalRequested superUser welcomeWithLink' (1 :: Int)
          superUser #> "@'SimpleX Directory' /pending"
          superUser <# "'SimpleX Directory'> > /pending"
          superUser <## "      1 registered group(s)"
          superUser <# "'SimpleX Directory'> 1. PSA (Privacy, Security & Anonymity)"
          superUser <## "Welcome message:"
          superUser <##. "Welcome! Link to join the group PSA: "
          superUser <## "Owner: bob"
          superUser <## "2 members"
          superUser <## "Status: pending admin approval"
          superUser <## "/'role 1', /'filter 1'"
          superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
          superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
          superUser <## "      Group approved!"
          bob <# "'SimpleX Directory'> The group ID 1 (PSA) is approved and listed in directory - please moderate it!"
          bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
          bob <## ""
          bob <## "Supported commands:"
          bob <## "/'filter 1' - to configure anti-spam filter."
          bob <## "/'role 1' - to set default member role."
          bob <## "/'link 1' - to view/upgrade group link."
          search bob "privacy" welcomeWithLink'
          search bob "security" welcomeWithLink'
          cath `connectVia` dsLink
          search cath "privacy" welcomeWithLink'
          bob #> "@'SimpleX Directory' /exec /contacts"
          bob <# "'SimpleX Directory'> > /exec /contacts"
          bob <## "      You are not allowed to use this command"
          superUser #> "@'SimpleX Directory' /exec /contacts"
          superUser <# "'SimpleX Directory'> > /exec /contacts"
          superUser <## "      alice (Alice)"
          superUser <## "bob (Bob)"
          superUser <## "cath (Catherine)"
    where
      search u s welcome = do
        u #> ("@'SimpleX Directory' " <> s)
        u <# ("'SimpleX Directory'> > " <> s)
        u <## "      Found 1 group(s)."
        u <# "'SimpleX Directory'> PSA (Privacy, Security & Anonymity)"
        u <## "Welcome message:"
        u <## welcome
        u <## "2 members"
      updateGroupProfile u welcome = do
        u ##> ("/set welcome #PSA " <> welcome)
        u <## "welcome message changed to:"
        u <## welcome
      approvalRequested su welcome grId = do
        su <# "'SimpleX Directory'> bob submitted the group ID 1:"
        su <## "PSA (Privacy, Security & Anonymity)"
        su <## "Welcome message:"
        su <## welcome
        su <## "2 members"
        su <## ""
        su <## "To approve send:"
        su <# ("'SimpleX Directory'> /approve 1:PSA " <> show grId)
  
  testSuspendResume :: HasCallStack => TestParams -> IO ()
  testSuspendResume ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        groupFound bob "privacy"
        superUser #> "@'SimpleX Directory' /suspend 1:privacy"
        superUser <# "'SimpleX Directory'> > /suspend 1:privacy"
        superUser <## "      Group suspended!"
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators."
        groupNotFound bob "privacy"
        superUser #> "@'SimpleX Directory' /resume 1:privacy"
        superUser <# "'SimpleX Directory'> > /resume 1:privacy"
        superUser <## "      Group listing resumed!"
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is listed in the directory again!"
        groupFound bob "privacy"
        superUser #> "@'SimpleX Directory' privacy"
        groupFoundN_ "" (Just 1) 2 superUser "privacy"
        superUser #> "@'SimpleX Directory' /link 1:privacy"
        superUser <# "'SimpleX Directory'> > /link 1:privacy"
        superUser <## "      The link to join the group ID 1 (privacy):"
        superUser <##. "https://localhost/g#"
        superUser <## "New member role: member"
        -- get and change the link to the equivalent - should not ask to re-approve
        bob #> "@'SimpleX Directory' /link 1"
        bob <# "'SimpleX Directory'> > /link 1"
        bob <## "      The link to join the group ID 1 (privacy):"
        gLink <- getTermLine bob
        gLink `shouldStartWith` "https://localhost/g#"
        bob <## "New member role: member"
        bob ##> "/show welcome #privacy"
        bob <## "Welcome message:"
        bob <## ("Link to join the group privacy: " <> gLink)
        bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true")
        bob <## "welcome message changed to:"
        bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true")
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
        bob <## "The group is listed in directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
        superUser <## "The group remained listed in directory."
        -- upgrade link
        -- make it upgradeable first
        superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
        superUser <# "'SimpleX Directory'> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
        superUser <## ""
        bob #> "@'SimpleX Directory' /link 1"
        bob <# "'SimpleX Directory'> > /link 1"
        bob <## "      The link to join the group ID 1 (privacy):"
        bob <##. "https://simplex.chat/contact#/"
        bob <## "New member role: member"
        bob <## "The link is being upgraded..."
        bob <# "'SimpleX Directory'> Please replace the old link in welcome message of your group ID 1 (privacy)"
        bob <## "If this is the only change, the group will remain listed in directory without re-approval."
        bob <## ""
        bob <## "The new link:"
        gLink' <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
        bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink')
        bob <## "welcome message changed to:"
        bob <## ("Link to join the group privacy: " <> gLink')
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
        bob <## "The group is listed in directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
        superUser <## "The group remained listed in directory."
        -- send message to group owner
        superUser #> "@'SimpleX Directory' /owner 1:privacy hello there"
        superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there"
        superUser <## "      Forwarded to @bob, the owner of the group ID 1 (privacy)"
        bob <# "'SimpleX Directory'> hello there"
  
  testDeleteGroup :: HasCallStack => TestParams -> IO ()
  testDeleteGroup ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        groupFound bob "privacy"
        bob #> "@'SimpleX Directory' /delete 1:privacy"
        bob <# "'SimpleX Directory'> > /delete 1:privacy"
        bob <## "      Your group privacy is deleted from the directory"
        groupNotFound bob "privacy"
  
  testDeleteGroupAdmin :: HasCallStack => TestParams -> IO ()
  testDeleteGroupAdmin ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          cath `connectVia` dsLink
          registerGroupId superUser cath "security" "Security" 2 1
          groupFound bob "privacy"
          groupFound bob "security"
          listUserGroup bob "privacy" "Privacy"
          listUserGroup cath "security" "Security"
          superUser #> "@'SimpleX Directory' /last"
          superUser <# "'SimpleX Directory'> > /last"
          superUser <## "      2 registered group(s)"
          memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active"
          memberGroupListing superUser cath 2 "security" "Security" 2 "active"
          -- trying to register group with the same name
          submitGroup bob "security" "Security"
          bob <# "'SimpleX Directory'> The group security (Security) is already listed in the directory, please choose another name."
          bob ##> "/d #security"
          bob <## "#security: you deleted the group"
          -- admin can delete the group
          superUser #> "@'SimpleX Directory' /delete 2:security"
          superUser <# "'SimpleX Directory'> > /delete 2:security"
          superUser <## "      The group security is deleted from the directory"
          groupFound cath "privacy"
          listUserGroup bob "privacy" "Privacy"
          groupNotFound bob "security"
          sendListCommand cath 0
          -- another user can register the group with the same name
          registerGroupId superUser bob "security" "Security" 4 2
  
  testSetRole :: HasCallStack => TestParams -> IO ()
  testSetRole ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          groupFound bob "privacy"
          bob #> "@'SimpleX Directory' /role 1:privacy observer"
          bob <# "'SimpleX Directory'> > /role 1:privacy observer"
          bob <## "      The initial member role for the group privacy is set to observer"
          bob <## ""
          note <- getTermLine bob
          let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
          cath ##> ("/c " <> groupLink)
          cath <## "connection request sent!"
          cath <## "#privacy: joining the group..."
          cath <## "#privacy: you joined the group"
          cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#"
          cath <## "#privacy: member bob (Bob) is connected"
          bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
          bob <## "#privacy: new member cath is connected"
          bob ##> "/ms #privacy"
          bob <## "bob (Bob): owner, you, created group"
          bob <## "'SimpleX Directory': admin, invited, connected"
          bob <## "cath (Catherine): observer, connected"
          cath ##> "#privacy hello"
          cath <## "#privacy: you don't have permission to send messages"
  
  testJoinGroup :: HasCallStack => TestParams -> IO ()
  testJoinGroup ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        withNewTestChat ps "cath" cathProfile $ \cath ->
          withNewTestChat ps "dan" danProfile $ \dan -> do
            bob `connectVia` dsLink
            registerGroup superUser bob "privacy" "Privacy"
            cath `connectVia` dsLink
            cath #> "@'SimpleX Directory' privacy"
            cath <# "'SimpleX Directory'> > privacy"
            cath <## "      Found 1 group(s)."
            cath <# "'SimpleX Directory'> privacy (Privacy)"
            cath <## "Welcome message:"
            welcomeMsg <- getTermLine cath
            let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg
            cath <## "2 members"
            cath ##> ("/c " <> groupLink)
            cath <## "connection request sent!"
            cath <## "#privacy: joining the group..."
            cath <## "#privacy: you joined the group"
            cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
            cath <## "use @'SimpleX Directory' <message> to send messages"
            cath <# ("#privacy 'SimpleX Directory'> " <> welcomeMsg)
            cath <## "#privacy: member bob (Bob) is connected"
            bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
            bob <## "#privacy: new member cath is connected"
            bob ##> "/create link #privacy"
            bobLink <- getGroupLink bob "privacy" GRMember True
            dan ##> ("/c " <> bobLink)
            dan <## "connection request sent!"
            concurrentlyN_
              [ do
                  bob <## "dan (Daniel): accepting request to join group #privacy..."
                  bob <## "#privacy: dan joined the group",
                do
                  dan <## "#privacy: joining the group..."
                  dan <## "#privacy: you joined the group"
                  dan <# ("#privacy bob> " <> welcomeMsg)
                  dan
                    <### [ "#privacy: member 'SimpleX Directory' is connected",
                           "#privacy: member cath (Catherine) is connected"
                         ],
                do
                  cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
                  cath <## "#privacy: new member dan is connected"
              ]
  
  testGroupNameWithSpaces :: HasCallStack => TestParams -> IO ()
  testGroupNameWithSpaces ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "Privacy & Security" ""
        groupFound bob "Privacy & Security"
        superUser #> "@'SimpleX Directory' /suspend 1:'Privacy & Security'"
        superUser <# "'SimpleX Directory'> > /suspend 1:'Privacy & Security'"
        superUser <## "      Group suspended!"
        bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators."
        groupNotFound bob "privacy"
        superUser #> "@'SimpleX Directory' /resume 1:'Privacy & Security'"
        superUser <# "'SimpleX Directory'> > /resume 1:'Privacy & Security'"
        superUser <## "      Group listing resumed!"
        bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is listed in the directory again!"
        groupFound bob "Privacy & Security"
  
  testSearchGroups :: HasCallStack => TestParams -> IO ()
  testSearchGroups ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob -> do
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          cath `connectVia` dsLink
          forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i
          connectUsers bob cath
          fullAddMember "MyGroup" "" bob cath GRMember
          joinGroup "MyGroup" cath bob
          cath <## "#MyGroup: member 'SimpleX Directory_1' is connected"
          cath <## "contact and member are merged: 'SimpleX Directory', #MyGroup 'SimpleX Directory_1'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          cath #> "@'SimpleX Directory' MyGroup"
          cath <# "'SimpleX Directory'> > MyGroup"
          cath <## "      Found 7 group(s), sending top 3."
          receivedGroup cath 0 3
          receivedGroup cath 1 2
          receivedGroup cath 2 2
          cath <# "'SimpleX Directory'> Send /next for 4 more result(s)."
          cath #> "@'SimpleX Directory' /next"
          cath <# "'SimpleX Directory'> > /next"
          cath <## "      Sending 3 more group(s)."
          receivedGroup cath 3 2
          receivedGroup cath 4 2
          receivedGroup cath 5 2
          cath <# "'SimpleX Directory'> Send /next for 1 more result(s)."
          -- search of another user does not affect the search of the first user
          groupFound bob "Another"
          cath #> "@'SimpleX Directory' ."
          cath <# "'SimpleX Directory'> > ."
          cath <## "      Sending 1 more group(s)."
          receivedGroup cath 6 2
          cath #> "@'SimpleX Directory' /all"
          cath <# "'SimpleX Directory'> > /all"
          cath <## "      8 group(s) listed, sending top 3."
          receivedGroup cath 0 3
          receivedGroup cath 1 2
          receivedGroup cath 2 2
          cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
          cath #> "@'SimpleX Directory' /new"
          cath <# "'SimpleX Directory'> > /new"
          cath <## "      8 group(s) listed, sending the most recent 3."
          receivedGroup cath 7 2
          receivedGroup cath 6 2
          receivedGroup cath 5 2
          cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
          cath #> "@'SimpleX Directory' term3"
          cath <# "'SimpleX Directory'> > term3"
          cath <## "      Found 3 group(s)."
          receivedGroup cath 4 2
          receivedGroup cath 5 2
          receivedGroup cath 6 2
          cath #> "@'SimpleX Directory' term1"
          cath <# "'SimpleX Directory'> > term1"
          cath <## "      Found 6 group(s), sending top 3."
          receivedGroup cath 1 2
          receivedGroup cath 2 2
          receivedGroup cath 3 2
          cath <# "'SimpleX Directory'> Send /next for 3 more result(s)."
          cath #> "@'SimpleX Directory' ."
          cath <# "'SimpleX Directory'> > ."
          cath <## "      Sending 3 more group(s)."
          receivedGroup cath 4 2
          receivedGroup cath 5 2
          receivedGroup cath 6 2
    where
      groups :: [String]
      groups =
        [ "MyGroup",
          "MyGroup term1 1",
          "MyGroup term1 2",
          "MyGroup term1 term2",
          "MyGroup term1 term2 term3",
          "MyGroup term1 term2 term3 term4",
          "MyGroup term1 term2 term3 term4 term5",
          "Another"
        ]
      receivedGroup :: TestCC -> Int -> Int -> IO ()
      receivedGroup u ix count = do
        u <#. ("'SimpleX Directory'> " <> groups !! ix)
        u <## "Welcome message:"
        u <##. "Link to join the group "
        u <## (show count <> " members")
  
  testInviteToOwnersGroup :: HasCallStack => TestParams -> IO ()
  testInviteToOwnersGroup ps =
    withDirectoryServiceCfgOwnersGroup ps testCfg True $ \superUser dsLink ->
      withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do
        bob `connectVia` dsLink
        registerGroupId superUser bob "privacy" "Privacy" 2 1
        bob <## "#owners: 'SimpleX Directory' invites you to join the group as member"
        bob <## "use /j owners to accept"
        superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners"
        bob ##> "/j owners"
        bob <## "#owners: you joined the group"
        bob <## "#owners: member alice (Alice) is connected"
        superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)"
        superUser <## "#owners: new member bob is connected"
        -- second group
        registerGroupId superUser bob "security" "Security" 3 2
        superUser <## "Owner is already a member of owners' group"
  
  testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO ()
  testDelistedOwnerLeaves ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          leaveGroup "privacy" bob
          cath <## "#privacy: bob left the group"
          bob <# "'SimpleX Directory'> You left the group ID 1 (privacy)."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner left)."
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupNotFound cath "privacy"
  
  testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO ()
  testDelistedOwnerRemoved ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          removeMember "privacy" cath bob
          bob <# "'SimpleX Directory'> You are removed from the group ID 1 (privacy)."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner is removed)."
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupNotFound cath "privacy"
  
  testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO ()
  testNotDelistedMemberLeaves ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          leaveGroup "privacy" cath
          bob <## "#privacy: cath left the group"
          (superUser </)
          cath `connectVia` dsLink
          cath #> "@'SimpleX Directory_1' privacy"
          groupFoundN_ "_1" Nothing 2 cath "privacy"
  
  testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO ()
  testNotDelistedMemberRemoved ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          removeMember "privacy" bob cath
          (superUser </)
          cath `connectVia` dsLink
          cath #> "@'SimpleX Directory_1' privacy"
          groupFoundN_ "_1" Nothing 2 cath "privacy"
  
  testDelistedServiceRemoved :: HasCallStack => TestParams -> IO ()
  testDelistedServiceRemoved ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          bob ##> "/rm #privacy 'SimpleX Directory'"
          bob <## "#privacy: you removed 'SimpleX Directory' from the group"
          cath <## "#privacy: bob removed 'SimpleX Directory' from the group"
          bob <# "'SimpleX Directory'> SimpleX Directory is removed from the group ID 1 (privacy)."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (directory service is removed)."
          cath `connectVia` dsLink
          groupNotFound_ "_1" cath "privacy"
  
  testDelistedGroupDeleted :: HasCallStack => TestParams -> IO ()
  testDelistedGroupDeleted ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          cath `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          connectUsers bob cath
          fullAddMember "privacy" "Privacy" bob cath GROwner
          joinGroup "privacy" cath bob
          cath <## "#privacy: member 'SimpleX Directory_1' is connected"
          cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          bob ##> "/d #privacy"
          bob <## "#privacy: you deleted the group"
          bob <# "'SimpleX Directory'> The group ID 1 (privacy) is deleted."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          cath <## "#privacy: bob deleted the group"
          cath <## "use /d #privacy to delete the local copy of the group"
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group is deleted)."
          groupNotFound cath "privacy"
  
  testDelistedRoleChanges :: HasCallStack => TestParams -> IO ()
  testDelistedRoleChanges ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupFoundN 3 cath "privacy"
          -- de-listed if service role changed
          bob ##> "/mr privacy 'SimpleX Directory' member"
          bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
          cath <## "#privacy: bob changed the role of 'SimpleX Directory' from admin to member"
          bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to member."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (SimpleX Directory role is changed to member)."
          groupNotFound cath "privacy"
          -- re-listed if service role changed back without profile changes
          cath ##> "/mr privacy 'SimpleX Directory' admin"
          cath <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
          bob <## "#privacy: cath changed the role of 'SimpleX Directory' from member to admin"
          bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
          bob <## ""
          bob <## "The group is listed in the directory again."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (SimpleX Directory role is changed to admin)."
          groupFoundN 3 cath "privacy"
          -- de-listed if owner role changed
          cath ##> "/mr privacy bob admin"
          cath <## "#privacy: you changed the role of bob to admin"
          bob <## "#privacy: cath changed your role from owner to admin"
          bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to admin."
          bob <## ""
          bob <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (user role is set to admin)."
          groupNotFound cath "privacy"
          -- re-listed if owner role changed back without profile changes
          cath ##> "/mr privacy bob owner"
          cath <## "#privacy: you changed the role of bob to owner"
          bob <## "#privacy: cath changed your role from admin to owner"
          bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to owner."
          bob <## ""
          bob <## "The group is listed in the directory again."
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (user role is set to owner)."
          groupFoundN 3 cath "privacy"
  
  testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO ()
  testNotDelistedMemberRoleChanged ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupFoundN 3 cath "privacy"
          bob ##> "/mr privacy cath member"
          bob <## "#privacy: you changed the role of cath to member"
          cath <## "#privacy: bob changed your role from owner to member"
          groupFoundN 3 cath "privacy"
  
  testNotSentApprovalBadRoles :: HasCallStack => TestParams -> IO ()
  testNotSentApprovalBadRoles ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          cath `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          welcomeWithLink <- groupAccepted bob "privacy"
          bob ##> "/mr privacy 'SimpleX Directory' member"
          bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
          updateProfileWithLink bob "privacy" welcomeWithLink 1
          bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
          bob ##> "/mr privacy 'SimpleX Directory' admin"
          bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
          bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
          bob <## ""
          bob <## "The group is submitted for approval."
          notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
          groupNotFound cath "privacy"
          approveRegistration superUser bob "privacy" 1
          groupFound cath "privacy"
  
  testNotApprovedBadRoles :: HasCallStack => TestParams -> IO ()
  testNotApprovedBadRoles ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          cath `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          welcomeWithLink <- groupAccepted bob "privacy"
          updateProfileWithLink bob "privacy" welcomeWithLink 1
          notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
          bob ##> "/mr privacy 'SimpleX Directory' member"
          bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
          let approve = "/approve 1:privacy 1"
          superUser #> ("@'SimpleX Directory' " <> approve)
          superUser <# ("'SimpleX Directory'> > " <> approve)
          superUser <## "      Group is not approved: SimpleX Directory is not an admin."
          groupNotFound cath "privacy"
          bob ##> "/mr privacy 'SimpleX Directory' admin"
          bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
          bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
          bob <## ""
          bob <## "The group is submitted for approval."
          notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
          approveRegistration superUser bob "privacy" 1
          groupFound cath "privacy"
  
  testRegOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
  testRegOwnerChangedProfile ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          bob ##> "/gp privacy privacy Privacy and Security"
          bob <## "description changed to: Privacy and Security"
          bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
          bob <## "It is hidden from the directory until approved."
          cath <## "bob updated group #privacy:"
          cath <## "description changed to: Privacy and Security"
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupNotFound cath "privacy"
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated."
          reapproveGroup 3 superUser bob
          groupFoundN 3 cath "privacy"
  
  testAnotherOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
  testAnotherOwnerChangedProfile ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          cath ##> "/gp privacy privacy Privacy and Security"
          cath <## "description changed to: Privacy and Security"
          bob <## "cath updated group #privacy:"
          bob <## "description changed to: Privacy and Security"
          bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
          bob <## "It is hidden from the directory until approved."
          groupNotFound cath "privacy"
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
          reapproveGroup 3 superUser bob
          groupFoundN 3 cath "privacy"
  
  testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
  testNotConnectedOwnerChangedProfile ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          withNewTestChat ps "dan" danProfile $ \dan -> do
            bob `connectVia` dsLink
            dan `connectVia` dsLink
            registerGroup superUser bob "privacy" "Privacy"
            addCathAsOwner bob cath
            cath ##> "/gp privacy privacy Privacy and Security"
            cath <## "description changed to: Privacy and Security"
            bob <## "cath updated group #privacy:"
            bob <## "description changed to: Privacy and Security"
            bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
            bob <## "It is hidden from the directory until approved."
            groupNotFound dan "privacy"
            superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
            reapproveGroup 3 superUser bob
            groupFoundN 3 dan "privacy"
  
  testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
  testRegOwnerRemovedLink ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          bob ##> "/show welcome #privacy"
          bob <## "Welcome message:"
          welcomeWithLink <- getTermLine bob
          bob ##> "/set welcome #privacy Welcome!"
          bob <## "welcome message changed to:"
          bob <## "Welcome!"
          bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message."
          bob <## ""
          bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
          cath <## "bob updated group #privacy:"
          cath <## "welcome message changed to:"
          cath <## "Welcome!"
          superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          groupNotFound cath "privacy"
          let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink
          bob ##> ("/set welcome #privacy " <> withChangedLink)
          bob <## "welcome message changed to:"
          bob <## withChangedLink
          bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
          bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          cath <## "bob updated group #privacy:"
          cath <## "welcome message changed to:"
          cath <## withChangedLink
          reapproveGroup 3 superUser bob
          groupFoundN 3 cath "privacy"
  
  testAnotherOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
  testAnotherOwnerRemovedLink ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          cath `connectVia` dsLink
          cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          bob ##> "/show welcome #privacy"
          bob <## "Welcome message:"
          welcomeWithLink <- getTermLine bob
          cath ##> "/set welcome #privacy Welcome!"
          cath <## "welcome message changed to:"
          cath <## "Welcome!"
          bob <## "cath updated group #privacy:"
          bob <## "welcome message changed to:"
          bob <## "Welcome!"
          bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
          bob <## ""
          bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
          superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
          groupNotFound cath "privacy"
          cath ##> ("/set welcome #privacy " <> welcomeWithLink)
          cath <## "welcome message changed to:"
          cath <## welcomeWithLink
          bob <## "cath updated group #privacy:"
          bob <## "welcome message changed to:"
          bob <## welcomeWithLink
          bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
          bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          reapproveGroup 3 superUser bob
          groupFoundN 3 cath "privacy"
  
  testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
  testNotConnectedOwnerRemovedLink ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          withNewTestChat ps "dan" danProfile $ \dan -> do
            bob `connectVia` dsLink
            dan `connectVia` dsLink
            registerGroup superUser bob "privacy" "Privacy"
            addCathAsOwner bob cath
            bob ##> "/show welcome #privacy"
            bob <## "Welcome message:"
            welcomeWithLink <- getTermLine bob
            cath ##> "/set welcome #privacy Welcome!"
            cath <## "welcome message changed to:"
            cath <## "Welcome!"
            bob <## "cath updated group #privacy:"
            bob <## "welcome message changed to:"
            bob <## "Welcome!"
            bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
            bob <## ""
            bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
            superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
            groupNotFound dan "privacy"
            cath ##> ("/set welcome #privacy " <> welcomeWithLink)
            cath <## "welcome message changed to:"
            cath <## welcomeWithLink
            bob <## "cath updated group #privacy:"
            bob <## "welcome message changed to:"
            bob <## welcomeWithLink
            -- bob <# "'SimpleX Directory'> The group link is added by another group member, your registration will not be processed."
            -- bob <## ""
            -- bob <## "Please update the group profile yourself."
            -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!")
            -- bob <## "welcome message changed to:"
            -- bob <## (welcomeWithLink <> " - welcome!")
            bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
            bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
            -- cath <## "bob updated group #privacy:"
            -- cath <## "welcome message changed to:"
            -- cath <## (welcomeWithLink <> " - welcome!")
            reapproveGroup 3 superUser bob
            groupFoundN 3 dan "privacy"
  
  testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO ()
  testDuplicateAskConfirmation ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          _ <- groupAccepted bob "privacy"
          cath `connectVia` dsLink
          submitGroup cath "privacy" "Privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
          cath <## "To confirm the registration, please send:"
          cath <# "'SimpleX Directory'> /confirm 1:privacy"
          cath #> "@'SimpleX Directory' /confirm 1:privacy"
          welcomeWithLink <- groupAccepted cath "privacy"
          groupNotFound bob "privacy"
          completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2
          groupFound bob "privacy"
  
  testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO ()
  testDuplicateProhibitRegistration ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          cath `connectVia` dsLink
          groupFound cath "privacy"
          _ <- submitGroup cath "privacy" "Privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
  
  testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO ()
  testDuplicateProhibitConfirmation ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          welcomeWithLink <- groupAccepted bob "privacy"
          cath `connectVia` dsLink
          submitGroup cath "privacy" "Privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
          cath <## "To confirm the registration, please send:"
          cath <# "'SimpleX Directory'> /confirm 1:privacy"
          groupNotFound cath "privacy"
          completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
          groupFound cath "privacy"
          cath #> "@'SimpleX Directory' /confirm 1:privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
  
  testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO ()
  testDuplicateProhibitWhenUpdated ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          welcomeWithLink <- groupAccepted bob "privacy"
          cath `connectVia` dsLink
          submitGroup cath "privacy" "Privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
          cath <## "To confirm the registration, please send:"
          cath <# "'SimpleX Directory'> /confirm 1:privacy"
          cath #> "@'SimpleX Directory' /confirm 1:privacy"
          welcomeWithLink' <- groupAccepted cath "privacy"
          groupNotFound cath "privacy"
          completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
          groupFound cath "privacy"
          cath ##> ("/set welcome privacy " <> welcomeWithLink')
          cath <## "welcome message changed to:"
          cath <## welcomeWithLink'
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
          cath ##> "/gp privacy security Security"
          cath <## "changed to #security (Security)"
          cath <# "'SimpleX Directory'> Thank you! The group link for ID 2 (security) is added to the welcome message."
          cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
          approveRegistration superUser cath "security" 2
          groupFound bob "security"
          groupFound cath "security"
  
  testDuplicateProhibitApproval :: HasCallStack => TestParams -> IO ()
  testDuplicateProhibitApproval ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          submitGroup bob "privacy" "Privacy"
          welcomeWithLink <- groupAccepted bob "privacy"
          cath `connectVia` dsLink
          submitGroup cath "privacy" "Privacy"
          cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
          cath <## "To confirm the registration, please send:"
          cath <# "'SimpleX Directory'> /confirm 1:privacy"
          cath #> "@'SimpleX Directory' /confirm 1:privacy"
          welcomeWithLink' <- groupAccepted cath "privacy"
          updateProfileWithLink cath "privacy" welcomeWithLink' 2
          notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2
          groupNotFound cath "privacy"
          completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
          groupFound cath "privacy"
          -- fails at approval, as already listed
          let approve = "/approve 2:privacy 1"
          superUser #> ("@'SimpleX Directory' " <> approve)
          superUser <# ("'SimpleX Directory'> > " <> approve)
          superUser <## "      The group ID 2 (privacy) is already listed in the directory."
  
  testListUserGroups :: HasCallStack => TestParams -> IO ()
  testListUserGroups ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          cath `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          connectUsers bob cath
          fullAddMember "privacy" "Privacy" bob cath GRMember
          joinGroup "privacy" cath bob
          cath <## "#privacy: member 'SimpleX Directory_1' is connected"
          cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          registerGroupId superUser bob "security" "Security" 2 2
          registerGroupId superUser cath "anonymity" "Anonymity" 3 1
          listUserGroup cath "anonymity" "Anonymity"
          -- with de-listed group
          groupFound cath "anonymity"
          cath ##> "/mr anonymity 'SimpleX Directory' member"
          cath <## "#anonymity: you changed the role of 'SimpleX Directory' to member"
          cath <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (anonymity) is changed to member."
          cath <## ""
          cath <## "The group is no longer listed in the directory."
          superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)."
          groupNotFound cath "anonymity"
          listGroups superUser bob cath
  
  testCapthaScreening :: HasCallStack => TestParams -> IO ()
  testCapthaScreening ps =
    withDirectoryService ps $ \superUser dsLink ->
      withNewTestChat ps "bob" bobProfile $ \bob ->
        withNewTestChat ps "cath" cathProfile $ \cath -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          -- check default role
          bob #> "@'SimpleX Directory' /role 1"
          bob <# "'SimpleX Directory'> > /role 1"
          bob <## "      The initial member role for the group privacy is set to member"
          bob <## "Send /'role 1 observer' to change it."
          bob <## ""
          note <- getTermLine bob
          let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
          -- enable captcha
          bob #> "@'SimpleX Directory' /filter 1 captcha"
          bob <# "'SimpleX Directory'> > /filter 1 captcha"
          bob <## "      Spam filter settings for group privacy set to:"
          bob <## "- reject long/inappropriate names: disabled"
          bob <## "- pass captcha to join: enabled"
          bob <## ""
          bob <## "/'filter 1 name' - enable name filter"
          bob <## "/'filter 1 name captcha' - enable both"
          bob <## "/'filter 1 off' - disable filter"
          -- connect with captcha screen
          _ <- join cath groupLink
          cath ##> "/_send #1(_support) text 123" -- sending incorrect captcha
          cath <# "#privacy (support) 123"
          cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123"
          cath <## "      Incorrect text, please try again."
          captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
          sendCaptcha cath captcha
          cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
          cath <## "#privacy: member bob (Bob) is connected"
          bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
          bob <## "#privacy: new member cath is connected"
          cath #> "#privacy hello"
          bob <# "#privacy cath> hello"
          cath ##> "/l privacy"
          cath <## "#privacy: you left the group"
          cath <## "use /d #privacy to delete the group"
          bob <## "#privacy: cath left the group"
          cath ##> "/d #privacy"
          cath <## "#privacy: you deleted the group"
          -- change default role to observer
          bob #> "@'SimpleX Directory' /role 1 observer"
          bob <# "'SimpleX Directory'> > /role 1 observer"
          bob <## "      The initial member role for the group privacy is set to observer"
          bob <## ""
          bob <##. "Please note: it applies only to members joining via this link: https://"
          -- connect with captcha screen again, as observer
          captcha' <- join cath groupLink
          sendCaptcha cath captcha'
          -- message from cath that left
          pastMember <- dropStrPrefix "#privacy: 'SimpleX Directory' forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath
          cath <# ("#privacy " <> pastMember <> "> hello [>>]")
          cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
          cath <## "#privacy: member bob (Bob) is connected"
          bob <## "#privacy: 'SimpleX Directory' added cath_1 (Catherine) to the group (connecting...)"
          bob <## "#privacy: new member cath_1 is connected"
          cath ##> "#privacy hello"
          cath <## "#privacy: you don't have permission to send messages"
          (bob </)
          cath ##> "/ms privacy"
          cath <## "cath (Catherine): observer, you, connected"
          cath <## "'SimpleX Directory': admin, host, connected"
          cath <## "bob (Bob): owner, connected"
          cath <## (pastMember <> ": author, status unknown")
    where
      join cath groupLink = do
        cath ##> ("/c " <> groupLink)
        cath <## "connection request sent!"
        cath <## "#privacy: joining the group..."
        cath <## "#privacy: you joined the group, pending approval"
        cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service."
        cath <## ""
        cath <## "Send captcha text to join the group privacy."
        dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
      sendCaptcha cath captcha = do
        cath ##> ("/_send #1(_support) text " <> captcha)
        cath <# ("#privacy (support) " <> captcha)
        cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
        cath <## "      Correct, you joined the group privacy"
        cath <## "#privacy: you joined the group"
  
  testRestoreDirectory :: HasCallStack => TestParams -> IO ()
  testRestoreDirectory ps = do
    testListUserGroups ps
    restoreDirectoryService ps 3 3 $ \superUser _dsLink ->
      withTestChat ps "bob" $ \bob ->
        withTestChat ps "cath" $ \cath -> do
          bob <## "2 contacts connected (use /cs for the list)"
          bob
            <### [ "#privacy: connected to server(s)",
                   "#security: connected to server(s)"
                 ]
          cath <## "2 contacts connected (use /cs for the list)"
          cath
            <### [ "#privacy: connected to server(s)",
                   "#anonymity: connected to server(s)"
                 ]
          listGroups superUser bob cath
          groupFoundN 3 bob "privacy"
          groupFound bob "security"
          groupFoundN 3 cath "privacy"
          cath #> "@'SimpleX Directory' security"
          groupFoundN' 2 cath "security"
  
  testCaptcha :: HasCallStack => TestParams -> IO ()
  testCaptcha _ps = do
    let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty"
    matchCaptchaStr captcha captcha `shouldBe` True
    matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True
    matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True
    matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True
    matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True
    matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True
    matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False
    matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False
    matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False
  
  listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
  listGroups superUser bob cath = do
    sendListCommand bob 2
    groupListing bob 1 "privacy" "Privacy" 3 "active"
    groupListing bob 2 "security" "Security" 2 "active"
    sendListCommand cath 1
    groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed"
    -- superuser lists all groups
    bob #> "@'SimpleX Directory' /last"
    bob <# "'SimpleX Directory'> > /last"
    bob <## "      You are not allowed to use this command"
    superUser #> "@'SimpleX Directory' /last"
    superUser <# "'SimpleX Directory'> > /last"
    superUser <## "      3 registered group(s)"
    memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active"
    memberGroupListing superUser bob 2 "security" "Security" 2 "active"
    memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"
    -- showing last 1 group
    superUser #> "@'SimpleX Directory' /last 1"
    superUser <# "'SimpleX Directory'> > /last 1"
    superUser <## "      3 registered group(s), showing the last 1"
    memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"
  
  listUserGroup :: HasCallStack => TestCC -> String -> String -> IO ()
  listUserGroup u n fn = do
    sendListCommand u 1
    groupListing u 1 n fn 2 "active"
  
  sendListCommand :: HasCallStack => TestCC -> Int -> IO ()
  sendListCommand u count = do
    u #> "@'SimpleX Directory' /list"
    u <# "'SimpleX Directory'> > /list"
    u <## ("      " <> show count <> " registered group(s)")
  
  groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO ()
  groupListing u = groupListing_ u Nothing
  
  memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO ()
  memberGroupListing su owner = groupListing_ su (Just owner)
  
  groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO ()
  groupListing_ su owner_ gId n fn count status = do
    su <# ("'SimpleX Directory'> " <> show gId <> ". " <> n <> " (" <> fn <> ")")
    su <## "Welcome message:"
    su <##. ("Link to join the group " <> n <> ": ")
    forM_ owner_ $ \owner -> do
      ownerName <- userName owner
      su <## ("Owner: " <> ownerName)
    su <## (show count <> " members")
    su <## ("Status: " <> status)
    su <## ("/'role " <> show gId <> "', /'filter " <> show gId <> "'")
  
  reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO ()
  reapproveGroup count superUser bob = do
    superUser <# "'SimpleX Directory'> bob submitted the group ID 1:"
    superUser <##. "privacy ("
    superUser <## "Welcome message:"
    superUser <##. "Link to join the group privacy: "
    superUser <## (show count <> " members")
    superUser <## ""
    superUser <## "To approve send:"
    superUser <# "'SimpleX Directory'> /approve 1:privacy 1"
    superUser #> "@'SimpleX Directory' /approve 1:privacy 1"
    superUser <# "'SimpleX Directory'> > /approve 1:privacy 1"
    superUser <## "      Group approved!"
    bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!"
    bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
    bob <## ""
    bob <## "Supported commands:"
    bob <## "/'filter 1' - to configure anti-spam filter."
    bob <## "/'role 1' - to set default member role."
    bob <## "/'link 1' - to view/upgrade group link."
  
  addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO ()
  addCathAsOwner bob cath = do
    connectUsers bob cath
    fullAddMember "privacy" "Privacy" bob cath GROwner
    joinGroup "privacy" cath bob
    cath <## "#privacy: member 'SimpleX Directory' is connected"
  
  withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO ()
  withDirectoryService ps = withDirectoryServiceCfg ps testCfg
  
  withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO ()
  withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False
  
  withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> (TestCC -> String -> IO ()) -> IO ()
  withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do
    dsLink <-
      withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds ->
        withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do
          connectUsers ds superUser
          when createOwnersGroup $ do
            superUser ##> "/g owners"
            superUser <## "group #owners is created"
            superUser <## "to add members use /a owners <name> or /create link #owners"
            superUser ##> "/a owners 'SimpleX Directory' admin"
            superUser <## "invitation to join the group #owners sent to 'SimpleX Directory'"
            ds <## "#owners: alice invites you to join the group as admin"
            ds <## "use /j owners to accept"
            ds ##> "/j owners"
            ds <## "#owners: you joined the group"
            superUser <## "#owners: 'SimpleX Directory' joined the group"
          ds ##> "/ad"
          getContactLink ds True
    withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test
  
  restoreDirectoryService :: HasCallStack => TestParams -> Int -> Int -> (TestCC -> String -> IO ()) -> IO ()
  restoreDirectoryService ps ctCount grCount test = do
    dsLink <-
      withTestChat ps serviceDbPrefix $ \ds -> do
        ds <## (show ctCount <> " contacts connected (use /cs for the list)")
        ds <## "Your address is active! To show: /sa"
        ds <## (show grCount <> " group links active")
        forM_ [1 .. grCount] $ \_ -> ds <##. "#"
        ds ##> "/sa"
        dsLink <- getContactLink ds False
        ds <## "auto_accept on"
        pure dsLink
    withDirectory ps testCfg dsLink test
  
  withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
  withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False
  
  withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> (TestCC -> String -> IO ()) -> IO ()
  withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test = do
    let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] $ if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing
    runDirectory cfg opts $
      withTestChatCfg ps cfg "super_user" $ \superUser -> do
        superUser <## "1 contacts connected (use /cs for the list)"
        when createOwnersGroup $
          superUser <## "#owners: connected to server(s)"
        test superUser dsLink
  
  runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
  runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
    st <- restoreDirectoryStore directoryLog
    t <- forkIO $ bot st
    threadDelay 500000
    action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
    where
      bot st = do
        env <- newServiceState opts
        let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}}
        simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env
  
  registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
  registerGroup su u n fn = registerGroupId su u n fn 1 1
  
  registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO ()
  registerGroupId su u n fn gId ugId = do
    submitGroup u n fn
    welcomeWithLink <- groupAccepted u n
    completeRegistrationId su u n fn welcomeWithLink gId ugId
  
  submitGroup :: TestCC -> String -> String -> IO ()
  submitGroup u n fn = do
    u ##> ("/g " <> viewName n <> if null fn then "" else " " <> fn)
    u <## ("group #" <> viewName n <> (if null fn then "" else " (" <> fn <> ")") <> " is created")
    u <## ("to add members use /a " <> viewName n <> " <name> or /create link #" <> viewName n)
    u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin")
    u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'")
  
  groupAccepted :: TestCC -> String -> IO String
  groupAccepted u n = do
    u <# ("'SimpleX Directory'> Joining the group " <> n <> "…")
    u <## ("#" <> viewName n <> ": 'SimpleX Directory' joined the group")
    u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…")
    u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
    u <## ""
    u <## "Please add it to the group welcome message."
    u <## "For example, add:"
    dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link
  
  completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
  completeRegistration su u n fn welcomeWithLink gId =
    completeRegistrationId su u n fn welcomeWithLink gId gId
  
  completeRegistrationId :: TestCC -> TestCC -> String -> String -> String -> Int -> Int -> IO ()
  completeRegistrationId su u n fn welcomeWithLink gId ugId = do
    updateProfileWithLink u n welcomeWithLink ugId
    notifySuperUser su u n fn welcomeWithLink gId
    approveRegistrationId su u n gId ugId
  
  updateProfileWithLink :: TestCC -> String -> String -> Int -> IO ()
  updateProfileWithLink u n welcomeWithLink ugId = do
    u ##> ("/set welcome " <> viewName n <> " " <> welcomeWithLink)
    u <## "welcome message changed to:"
    u <## welcomeWithLink
    u <# ("'SimpleX Directory'> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.")
    u <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
  
  notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
  notifySuperUser su u n fn welcomeWithLink gId = do
    uName <- userName u
    su <# ("'SimpleX Directory'> " <> uName <> " submitted the group ID " <> show gId <> ":")
    su <## (n <> if null fn then "" else " (" <> fn <> ")")
    su <## "Welcome message:"
    su <## welcomeWithLink
    su .<## "members"
    su <## ""
    su <## "To approve send:"
    let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
    su <# ("'SimpleX Directory'> " <> approve)
  
  approveRegistration :: TestCC -> TestCC -> String -> Int -> IO ()
  approveRegistration su u n gId =
    approveRegistrationId su u n gId gId
  
  approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO ()
  approveRegistrationId su u n gId ugId = do
    let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
    su #> ("@'SimpleX Directory' " <> approve)
    su <# ("'SimpleX Directory'> > " <> approve)
    su <## "      Group approved!"
    u <# ("'SimpleX Directory'> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!")
    u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
    u <## ""
    u <## "Supported commands:"
    u <## ("/'filter " <> show ugId <> "' - to configure anti-spam filter.")
    u <## ("/'role " <> show ugId <> "' - to set default member role.")
    u <## ("/'link " <> show ugId <> "' - to view/upgrade group link.")
  
  connectVia :: TestCC -> String -> IO ()
  u `connectVia` dsLink = do
    u ##> ("/c " <> dsLink)
    u <## "connection request sent!"
    u .<## ": contact is connected"
    u .<# "> Welcome to SimpleX Directory!"
    u <## ""
    u <## "🔍 Send search string to find groups - try security."
    u <## "/help - how to submit your group."
    u <## "/new - recent groups."
    u <## ""
    u <## "[Directory rules](https://simplex.chat/docs/directory.html)."
  
  joinGroup :: String -> TestCC -> TestCC -> IO ()
  joinGroup gName member host = do
    let gn = "#" <> gName
    memberName <- userName member
    hostName <- userName host
    member ##> ("/j " <> gName)
    member <## (gn <> ": you joined the group")
    member <#. (gn <> " " <> hostName <> "> Link to join the group " <> gName <> ": ")
    host <## (gn <> ": " <> memberName <> " joined the group")
  
  leaveGroup :: String -> TestCC -> IO ()
  leaveGroup gName member = do
    let gn = "#" <> gName
    member ##> ("/l " <> gName)
    member <## (gn <> ": you left the group")
    member <## ("use /d " <> gn <> " to delete the group")
  
  removeMember :: String -> TestCC -> TestCC -> IO ()
  removeMember gName admin removed = do
    let gn = "#" <> gName
    adminName <- userName admin
    removedName <- userName removed
    admin ##> ("/rm " <> gName <> " " <> removedName)
    admin <## (gn <> ": you removed " <> removedName <> " from the group")
    removed <## (gn <> ": " <> adminName <> " removed you from the group")
    removed <## ("use /d " <> gn <> " to delete the group")
  
  groupFound :: TestCC -> String -> IO ()
  groupFound = groupFoundN 2
  
  groupFoundN :: Int -> TestCC -> String -> IO ()
  groupFoundN count u name = do
    u #> ("@'SimpleX Directory' " <> name)
    groupFoundN' count u name
  
  groupFoundN' :: Int -> TestCC -> String -> IO ()
  groupFoundN' = groupFoundN_ "" Nothing
  
  groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO ()
  groupFoundN_ suffix shownId_ count u name = do
    u <# ("'SimpleX Directory" <> suffix <> "'> > " <> name)
    u <## "      Found 1 group(s)."
    u <#. ("'SimpleX Directory" <> suffix <> "'> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name)
    u <## "Welcome message:"
    u <##. "Link to join the group "
    u <## (show count <> " members")
  
  groupNotFound :: TestCC -> String -> IO ()
  groupNotFound = groupNotFound_ ""
  
  groupNotFound_ :: String -> TestCC -> String -> IO ()
  groupNotFound_ suffix u s = do
    u #> ("@'SimpleX Directory" <> suffix <> "' " <> s)
    u <# ("'SimpleX Directory" <> suffix <> "'> > " <> s)
    u <## "      No groups found"
  