{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}

module ChatTests.Profiles where

import ChatClient
import ChatTests.Utils
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import Control.Monad
import Control.Monad.Except
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Util (encodeJSON)
import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it)

chatProfileTests :: SpecWith FilePath
chatProfileTests = do
  describe "user profiles" $ do
    it "update user profile and notify contacts" testUpdateProfile
    it "update user profile with image" testUpdateProfileImage
    it "use multiword profile names" testMultiWordProfileNames
  describe "user contact link" $ do
    it "create and connect via contact link" testUserContactLink
    it "add contact link to profile" testProfileLink
    it "auto accept contact requests" testUserContactLinkAutoAccept
    it "deduplicate contact requests" testDeduplicateContactRequests
    it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange
    it "reject contact and delete contact link" testRejectContactAndDeleteUserContact
    it "delete connection requests when contact link deleted" testDeleteConnectionRequests
    it "auto-reply message" testAutoReplyMessage
    it "auto-reply message in incognito" testAutoReplyMessageInIncognito
  describe "contact address connection plan" $ do
    it "contact address ok to connect; known contact" testPlanAddressOkKnown
    it "own contact address" testPlanAddressOwn
    it "connecting via contact address" testPlanAddressConnecting
    it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow
    it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected
    it "contact via address" testPlanAddressContactViaAddress
  describe "incognito" $ do
    it "connect incognito via invitation link" testConnectIncognitoInvitationLink
    it "connect incognito via contact address" testConnectIncognitoContactAddress
    it "accept contact request incognito" testAcceptContactRequestIncognito
    it "set connection incognito" testSetConnectionIncognito
    it "reset connection incognito" testResetConnectionIncognito
    it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation
    it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow
    it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors
    it "set, reset, set connection incognito" testSetResetSetConnectionIncognito
    it "join group incognito" testJoinGroupIncognito
    it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito
    it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito
    it "deleting contact first, group second deletes incognito profile" testDeleteContactThenGroupDeletesIncognitoProfile
    it "deleting group first, contact second deletes incognito profile" testDeleteGroupThenContactDeletesIncognitoProfile
  describe "contact aliases" $ do
    it "set contact alias" testSetAlias
    it "set connection alias" testSetConnectionAlias
  describe "pending connection users" $ do
    it "change user for pending connection" testChangePCCUser
    it "change from incognito profile connects as new user" testChangePCCUserFromIncognito
    it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito
    it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv
  describe "preferences" $ do
    it "set contact preferences" testSetContactPrefs
    it "feature offers" testFeatureOffers
    it "update group preferences" testUpdateGroupPrefs
    it "allow full deletion to contact" testAllowFullDeletionContact
    it "allow full deletion to group" testAllowFullDeletionGroup
    it "prohibit direct messages to group members" testProhibitDirectMessages
    xit'' "enable timed messages with contact" testEnableTimedMessagesContact
    it "enable timed messages in group" testEnableTimedMessagesGroup
    xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally
    it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs
    describe "group preferences for specific member role" $ do
      it "direct messages" testGroupPrefsDirectForRole
      it "files & media" testGroupPrefsFilesForRole
      it "SimpleX links" testGroupPrefsSimplexLinksForRole
    it "set user, contact and group UI theme" testSetUITheme

testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile =
  testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
    \alice bob cath -> do
      createGroup3 "team" alice bob cath
      alice ##> "/p"
      alice <## "user profile: alice (Alice)"
      alice <## "use /p <display name> to change it"
      alice <## "(the updated profile will be sent to all your contacts)"
      alice ##> "/p alice"
      concurrentlyN_
        [ alice <## "user full name removed (your 2 contacts are notified)",
          bob <## "contact alice removed full name",
          cath <## "contact alice removed full name"
        ]
      alice ##> "/p alice Alice Jones"
      concurrentlyN_
        [ alice <## "user full name changed to Alice Jones (your 2 contacts are notified)",
          bob <## "contact alice updated full name: Alice Jones",
          cath <## "contact alice updated full name: Alice Jones"
        ]
      cath ##> "/p cate"
      concurrentlyN_
        [ cath <## "user profile is changed to cate (your 2 contacts are notified)",
          do
            alice <## "contact cath changed to cate"
            alice <## "use @cate <message> to send messages",
          do
            bob <## "contact cath changed to cate"
            bob <## "use @cate <message> to send messages"
        ]
      cath ##> "/p cat Cate"
      concurrentlyN_
        [ cath <## "user profile is changed to cat (Cate) (your 2 contacts are notified)",
          do
            alice <## "contact cate changed to cat (Cate)"
            alice <## "use @cat <message> to send messages",
          do
            bob <## "contact cate changed to cat (Cate)"
            bob <## "use @cat <message> to send messages"
        ]

testUpdateProfileImage :: HasCallStack => FilePath -> IO ()
testUpdateProfileImage =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      connectUsers alice bob
      alice ##> "/set profile image data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="
      alice <## "profile image updated"
      alice ##> "/show profile image"
      alice <## "Profile image:"
      alice <## "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="
      alice ##> "/delete profile image"
      alice <## "profile image removed"
      alice ##> "/show profile image"
      alice <## "No profile image"
      alice ##> "/_profile 1 {\"displayName\": \"alice2\", \"fullName\": \"\", \"preferences\": {\"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
      alice <## "user profile is changed to alice2 (your 1 contacts are notified)"
      bob <## "contact alice changed to alice2"
      bob <## "use @alice2 <message> to send messages"
      (bob </)

testMultiWordProfileNames :: HasCallStack => FilePath -> IO ()
testMultiWordProfileNames =
  testChat3 aliceProfile' bobProfile' cathProfile' $
    \alice bob cath -> do
      alice ##> "/c"
      inv <- getInvitation alice
      bob ##> ("/c " <> inv)
      bob <## "confirmation sent!"
      concurrently_
        (bob <## "'Alice Jones': contact is connected")
        (alice <## "'Bob James': contact is connected")
      alice #> "@'Bob James' hi"
      bob <# "'Alice Jones'> hi"
      alice ##> "/g 'Our Team'"
      alice <## "group #'Our Team' is created"
      alice <## "to add members use /a 'Our Team' <name> or /create link #'Our Team'"
      alice ##> "/a 'Our Team' 'Bob James' admin"
      alice <## "invitation to join the group #'Our Team' sent to 'Bob James'"
      bob <## "#'Our Team': 'Alice Jones' invites you to join the group as admin"
      bob <## "use /j 'Our Team' to accept"
      bob ##> "/j 'Our Team'"
      bob <## "#'Our Team': you joined the group"
      alice <## "#'Our Team': 'Bob James' joined the group"
      bob ##> "/c"
      inv' <- getInvitation bob
      cath ##> ("/c " <> inv')
      cath <## "confirmation sent!"
      concurrently_
        (cath <## "'Bob James': contact is connected")
        (bob <## "'Cath Johnson': contact is connected")
      bob ##> "/a 'Our Team' 'Cath Johnson'"
      bob <## "invitation to join the group #'Our Team' sent to 'Cath Johnson'"
      cath <## "#'Our Team': 'Bob James' invites you to join the group as member"
      cath <## "use /j 'Our Team' to accept"
      cath ##> "/j 'Our Team'"
      concurrentlyN_
        [ bob <## "#'Our Team': 'Cath Johnson' joined the group",
          do
            cath <## "#'Our Team': you joined the group"
            cath <## "#'Our Team': member 'Alice Jones' is connected",
          do
            alice <## "#'Our Team': 'Bob James' added 'Cath Johnson' to the group (connecting...)"
            alice <## "#'Our Team': new member 'Cath Johnson' is connected"
        ]
      bob #> "#'Our Team' hi"
      alice <# "#'Our Team' 'Bob James'> hi"
      cath <# "#'Our Team' 'Bob James'> hi"
      alice `send` "@'Cath Johnson' hello"
      alice
        <### [ "member #'Our Team' 'Cath Johnson' does not have direct connection, creating",
               "contact for member #'Our Team' 'Cath Johnson' is created",
               "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'",
               WithTime "@'Cath Johnson' hello"
             ]
      cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you"
      cath <# "'Alice Jones'> hello"
      cath <## "'Alice Jones': you can send messages to contact"
      cath <## "'Alice Jones': contact is connected"
      alice <## "'Cath Johnson': contact is connected"
      cath ##> "/p 'Cath J'"
      cath <## "user profile is changed to 'Cath J' (your 2 contacts are notified)"
      alice <## "contact 'Cath Johnson' changed to 'Cath J'"
      alice <## "use @'Cath J' <message> to send messages"
      bob <## "contact 'Cath Johnson' changed to 'Cath J'"
      bob <## "use @'Cath J' <message> to send messages"
      alice #> "@'Cath J' hi"
      cath <# "'Alice Jones'> hi"
  where
    aliceProfile' = baseProfile {displayName = "Alice Jones"}
    bobProfile' = baseProfile {displayName = "Bob James"}
    cathProfile' = baseProfile {displayName = "Cath Johnson"}
    baseProfile = Profile {displayName = "", fullName = "", image = Nothing, contactLink = Nothing, preferences = defaultPrefs}

testUserContactLink :: HasCallStack => FilePath -> IO ()
testUserContactLink =
  testChat3 aliceProfile bobProfile cathProfile $
    \alice bob cath -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True
      bob ##> ("/c " <> cLink)
      alice <#? bob
      alice @@@ [("<@bob", "")]
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      threadDelay 100000
      alice @@@ [("@bob", lastChatFeature)]
      alice <##> bob

      cath ##> ("/c " <> cLink)
      alice <#? cath
      alice @@@ [("<@cath", ""), ("@bob", "hey")]
      alice ##> "/ac cath"
      alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
      concurrently_
        (cath <## "alice (Alice): contact is connected")
        (alice <## "cath (Catherine): contact is connected")
      threadDelay 100000
      alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
      alice <##> cath

testProfileLink :: HasCallStack => FilePath -> IO ()
testProfileLink =
  testChat3 aliceProfile bobProfile cathProfile $
    \alice bob cath -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True

      bob ##> ("/c " <> cLink)
      alice <#? bob
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      alice <##> bob

      alice ##> "/pa on"
      alice <## "new contact address set"

      bob <## "alice set new contact address, use /info alice to view"
      checkAliceProfileLink bob cLink

      cath ##> ("/c " <> cLink)
      alice <#? cath
      alice ##> "/ac cath"
      alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
      concurrently_
        (cath <## "alice (Alice): contact is connected")
        (alice <## "cath (Catherine): contact is connected")
      alice <##> cath

      checkAliceProfileLink cath cLink

      alice ##> "/pa off"
      alice <## "contact address removed"

      bob <## "alice removed contact address"
      checkAliceNoProfileLink bob

      cath <## "alice removed contact address"
      checkAliceNoProfileLink cath

      alice ##> "/pa on"
      alice <## "new contact address set"

      bob <## "alice set new contact address, use /info alice to view"
      checkAliceProfileLink bob cLink

      cath <## "alice set new contact address, use /info alice to view"
      checkAliceProfileLink cath cLink

      alice ##> "/da"
      alice <## "Your chat address is deleted - accepted contacts will remain connected."
      alice <## "To create a new chat address use /ad"

      bob <## "alice removed contact address"
      checkAliceNoProfileLink bob

      cath <## "alice removed contact address"
      checkAliceNoProfileLink cath
  where
    checkAliceProfileLink cc cLink = do
      cc ##> "/info alice"
      cc <## "contact ID: 2"
      cc <##. "receiving messages via"
      cc <##. "sending messages via"
      cc <## ("contact address: " <> cLink)
      cc <## "you've shared main profile with this contact"
      cc <## "connection not verified, use /code command to see security code"
      cc <## "quantum resistant end-to-end encryption"
      cc <## currentChatVRangeInfo
    checkAliceNoProfileLink cc = do
      cc ##> "/info alice"
      cc <## "contact ID: 2"
      cc <##. "receiving messages via"
      cc <##. "sending messages via"
      cc <## "you've shared main profile with this contact"
      cc <## "connection not verified, use /code command to see security code"
      cc <## "quantum resistant end-to-end encryption"
      cc <## currentChatVRangeInfo

testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO ()
testUserContactLinkAutoAccept =
  testChat4 aliceProfile bobProfile cathProfile danProfile $
    \alice bob cath dan -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True

      bob ##> ("/c " <> cLink)
      alice <#? bob
      alice @@@ [("<@bob", "")]
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      threadDelay 100000
      alice @@@ [("@bob", lastChatFeature)]
      alice <##> bob

      alice ##> "/auto_accept on"
      alice <## "auto_accept on"

      cath ##> ("/c " <> cLink)
      cath <## "connection request sent!"
      alice <## "cath (Catherine): accepting contact request..."
      alice <## "cath (Catherine): you can send messages to contact"
      concurrently_
        (cath <## "alice (Alice): contact is connected")
        (alice <## "cath (Catherine): contact is connected")
      threadDelay 100000
      alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
      alice <##> cath

      alice ##> "/auto_accept off"
      alice <## "auto_accept off"

      dan ##> ("/c " <> cLink)
      alice <#? dan
      alice @@@ [("<@dan", ""), ("@cath", "hey"), ("@bob", "hey")]
      alice ##> "/ac dan"
      alice <## "dan (Daniel): accepting contact request, you can send messages to contact"
      concurrently_
        (dan <## "alice (Alice): contact is connected")
        (alice <## "dan (Daniel): contact is connected")
      threadDelay 100000
      alice @@@ [("@dan", lastChatFeature), ("@cath", "hey"), ("@bob", "hey")]
      alice <##> dan

testDeduplicateContactRequests :: HasCallStack => FilePath -> IO ()
testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True

    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice @@@ [("<@bob", "")]
    bob @@@! [(":1", "", Just ConnJoined)]

    bob ##> ("/c " <> cLink)
    alice <#? bob
    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice @@@ [("<@bob", "")]
    bob @@@! [(":3", "", Just ConnJoined), (":2", "", Just ConnJoined), (":1", "", Just ConnJoined)]

    alice ##> "/ac bob"
    alice <## "bob (Bob): accepting contact request, you can send messages to contact"
    concurrently_
      (bob <## "alice (Alice): contact is connected")
      (alice <## "bob (Bob): contact is connected")

    bob ##> ("/c " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"
    threadDelay 100000
    alice @@@ [("@bob", lastChatFeature)]
    bob @@@ [("@alice", lastChatFeature), (":2", ""), (":1", "")]
    bob ##> "/_delete :1"
    bob <## "connection :1 deleted"
    bob ##> "/_delete :2"
    bob <## "connection :2 deleted"

    alice <##> bob
    alice @@@ [("@bob", "hey")]
    bob @@@ [("@alice", "hey")]

    bob ##> ("/c " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"

    alice <##> bob
    alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")])
    bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")])

    cath ##> ("/c " <> cLink)
    alice <#? cath
    alice @@@ [("<@cath", ""), ("@bob", "hey")]
    alice ##> "/ac cath"
    alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
    concurrently_
      (cath <## "alice (Alice): contact is connected")
      (alice <## "cath (Catherine): contact is connected")
    threadDelay 100000
    alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
    alice <##> cath

testDeduplicateContactRequestsProfileChange :: HasCallStack => FilePath -> IO ()
testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True

    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice @@@ [("<@bob", "")]

    bob ##> "/p bob"
    bob <## "user full name removed (your 0 contacts are notified)"
    bob ##> ("/c " <> cLink)
    bob <## "connection request sent!"
    alice <## "bob wants to connect to you!"
    alice <## "to accept: /ac bob"
    alice <## "to reject: /rc bob (the sender will NOT be notified)"
    alice @@@ [("<@bob", "")]

    bob ##> "/p bob Bob Ross"
    bob <## "user full name changed to Bob Ross (your 0 contacts are notified)"
    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice @@@ [("<@bob", "")]

    bob ##> "/p robert Robert"
    bob <## "user profile is changed to robert (Robert) (your 0 contacts are notified)"
    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice @@@ [("<@robert", "")]

    alice ##> "/ac bob"
    alice <## "no contact request from bob"
    alice ##> "/ac robert"
    alice <## "robert (Robert): accepting contact request, you can send messages to contact"
    concurrently_
      (bob <## "alice (Alice): contact is connected")
      (alice <## "robert (Robert): contact is connected")

    bob ##> ("/c " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"
    threadDelay 100000
    alice @@@ [("@robert", lastChatFeature)]
    bob @@@ [("@alice", lastChatFeature), (":3", ""), (":2", ""), (":1", "")]
    bob ##> "/_delete :1"
    bob <## "connection :1 deleted"
    bob ##> "/_delete :2"
    bob <## "connection :2 deleted"
    bob ##> "/_delete :3"
    bob <## "connection :3 deleted"

    alice <##> bob
    alice @@@ [("@robert", "hey")]
    bob @@@ [("@alice", "hey")]

    bob ##> ("/c " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"

    alice <##> bob
    threadDelay 100000
    alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")])
    bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")])

    cath ##> ("/c " <> cLink)
    alice <#? cath
    alice @@@ [("<@cath", ""), ("@robert", "hey")]
    alice ##> "/ac cath"
    alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
    concurrently_
      (cath <## "alice (Alice): contact is connected")
      (alice <## "cath (Catherine): contact is connected")
    threadDelay 100000
    alice @@@ [("@cath", lastChatFeature), ("@robert", "hey")]
    alice <##> cath

testRejectContactAndDeleteUserContact :: HasCallStack => FilePath -> IO ()
testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/_address 1"
    cLink <- getContactLink alice True
    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice ##> "/rc bob"
    alice <## "bob: contact request rejected"
    (bob </)

    alice ##> "/_show_address 1"
    cLink' <- getContactLink alice False
    alice <## "auto_accept off"
    cLink' `shouldBe` cLink

    alice ##> "/_delete_address 1"
    alice <## "Your chat address is deleted - accepted contacts will remain connected."
    alice <## "To create a new chat address use /ad"

    cath ##> ("/c " <> cLink)
    cath <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"

testDeleteConnectionRequests :: HasCallStack => FilePath -> IO ()
testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True
    bob ##> ("/c " <> cLink)
    alice <#? bob
    cath ##> ("/c " <> cLink)
    alice <#? cath

    alice ##> "/da"
    alice <## "Your chat address is deleted - accepted contacts will remain connected."
    alice <## "To create a new chat address use /ad"

    alice ##> "/ad"
    cLink' <- getContactLink alice True
    bob ##> ("/c " <> cLink')
    -- same names are used here, as they were released at /da
    alice <#? bob
    cath ##> ("/c " <> cLink')
    alice <#? cath

testAutoReplyMessage :: HasCallStack => FilePath -> IO ()
testAutoReplyMessage = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True
    alice ##> "/_auto_accept 1 on incognito=off text hello!"
    alice <## "auto_accept on"
    alice <## "auto reply:"
    alice <## "hello!"

    bob ##> ("/c " <> cLink)
    bob <## "connection request sent!"
    alice <## "bob (Bob): accepting contact request..."
    alice <## "bob (Bob): you can send messages to contact"
    alice <# "@bob hello!"
    concurrentlyN_
      [ do
          bob <# "alice> hello!"
          bob <## "alice (Alice): contact is connected",
        alice <## "bob (Bob): contact is connected"
      ]

testAutoReplyMessageInIncognito :: HasCallStack => FilePath -> IO ()
testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True
    alice ##> "/auto_accept on incognito=on text hello!"
    alice <## "auto_accept on, incognito"
    alice <## "auto reply:"
    alice <## "hello!"

    bob ##> ("/c " <> cLink)
    bob <## "connection request sent!"
    alice <## "bob (Bob): accepting contact request..."
    alice <## "bob (Bob): you can send messages to contact"
    alice <# "i @bob hello!"
    aliceIncognito <- getTermLine alice
    concurrentlyN_
      [ do
          bob <# (aliceIncognito <> "> hello!")
          bob <## (aliceIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## "use /i bob to print out this incognito profile again"
      ]

testPlanAddressOkKnown :: HasCallStack => FilePath -> IO ()
testPlanAddressOkKnown =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True

      bob ##> ("/_connect plan 1 " <> cLink)
      bob <## "contact address: ok to connect"

      bob ##> ("/c " <> cLink)
      alice <#? bob
      alice @@@ [("<@bob", "")]
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      alice <##> bob

      bob ##> ("/_connect plan 1 " <> cLink)
      bob <## "contact address: known contact alice"
      bob <## "use @alice <message> to send messages"

      let cLinkSchema2 = linkAnotherSchema cLink
      bob ##> ("/_connect plan 1 " <> cLinkSchema2)
      bob <## "contact address: known contact alice"
      bob <## "use @alice <message> to send messages"

      bob ##> ("/c " <> cLink)
      bob <## "contact address: known contact alice"
      bob <## "use @alice <message> to send messages"

testPlanAddressOwn :: HasCallStack => FilePath -> IO ()
testPlanAddressOwn tmp =
  withNewTestChat tmp "alice" aliceProfile $ \alice -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True

    alice ##> ("/_connect plan 1 " <> cLink)
    alice <## "contact address: own address"

    let cLinkSchema2 = linkAnotherSchema cLink
    alice ##> ("/_connect plan 1 " <> cLinkSchema2)
    alice <## "contact address: own address"

    alice ##> ("/c " <> cLink)
    alice <## "connection request sent!"
    alice <## "alice_1 (Alice) wants to connect to you!"
    alice <## "to accept: /ac alice_1"
    alice <## "to reject: /rc alice_1 (the sender will NOT be notified)"
    alice @@@ [("<@alice_1", ""), (":2", "")]
    alice ##> "/ac alice_1"
    alice <## "alice_1 (Alice): accepting contact request, you can send messages to contact"
    alice
      <### [ "alice_1 (Alice): contact is connected",
             "alice_2 (Alice): contact is connected"
           ]

    threadDelay 100000
    alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)]
    alice `send` "@alice_2 hi"
    alice
      <### [ WithTime "@alice_2 hi",
             WithTime "alice_1> hi"
           ]
    alice `send` "@alice_1 hey"
    alice
      <### [ WithTime "@alice_1 hey",
             WithTime "alice_2> hey"
           ]
    alice @@@ [("@alice_1", "hey"), ("@alice_2", "hey")]

    alice ##> ("/_connect plan 1 " <> cLink)
    alice <## "contact address: own address"

    alice ##> ("/c " <> cLink)
    alice <## "alice_2 (Alice): contact already exists"

testPlanAddressConnecting :: HasCallStack => FilePath -> IO ()
testPlanAddressConnecting tmp = do
  cLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
    alice ##> "/ad"
    getContactLink alice True
  withNewTestChat tmp "bob" bobProfile $ \bob -> do
    threadDelay 100000

    bob ##> ("/c " <> cLink)
    bob <## "connection request sent!"

    bob ##> ("/_connect plan 1 " <> cLink)
    bob <## "contact address: connecting, allowed to reconnect"

    let cLinkSchema2 = linkAnotherSchema cLink
    bob ##> ("/_connect plan 1 " <> cLinkSchema2)
    bob <## "contact address: connecting, allowed to reconnect"

    threadDelay 100000
  withTestChat tmp "alice" $ \alice -> do
    alice <## "Your address is active! To show: /sa"
    alice <## "bob (Bob) wants to connect to you!"
    alice <## "to accept: /ac bob"
    alice <## "to reject: /rc bob (the sender will NOT be notified)"
    alice ##> "/ac bob"
    alice <## "bob (Bob): accepting contact request, you can send messages to contact"
  withTestChat tmp "bob" $ \bob -> do
    threadDelay 500000
    bob <## "alice (Alice): contact is connected"
    bob @@@ [("@alice", "Audio/video calls: enabled")]
    bob ##> ("/_connect plan 1 " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"

    let cLinkSchema2 = linkAnotherSchema cLink
    bob ##> ("/_connect plan 1 " <> cLinkSchema2)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"

    bob ##> ("/c " <> cLink)
    bob <## "contact address: known contact alice"
    bob <## "use @alice <message> to send messages"

testPlanAddressConnectingSlow :: HasCallStack => FilePath -> IO ()
testPlanAddressConnectingSlow tmp = do
  cLink <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do
    alice ##> "/ad"
    getContactLink alice True
  withNewTestChatCfg tmp testCfgSlow "bob" bobProfile $ \bob -> do
    threadDelay 100000

    bob ##> ("/c " <> cLink)
    bob <## "connection request sent!"

    bob ##> ("/_connect plan 1 " <> cLink)
    bob <## "contact address: connecting, allowed to reconnect"

    let cLinkSchema2 = linkAnotherSchema cLink
    bob ##> ("/_connect plan 1 " <> cLinkSchema2)
    bob <## "contact address: connecting, allowed to reconnect"

    threadDelay 100000
  withTestChatCfg tmp testCfgSlow "alice" $ \alice -> do
    alice <## "Your address is active! To show: /sa"
    alice <## "bob (Bob) wants to connect to you!"
    alice <## "to accept: /ac bob"
    alice <## "to reject: /rc bob (the sender will NOT be notified)"
    alice ##> "/ac bob"
    alice <## "bob (Bob): accepting contact request..."
  withTestChatCfg tmp testCfgSlow "bob" $ \bob -> do
    threadDelay 500000
    bob @@@ [("@alice", "")]
    bob ##> ("/_connect plan 1 " <> cLink)
    bob <## "contact address: connecting to contact alice"

    let cLinkSchema2 = linkAnotherSchema cLink
    bob ##> ("/_connect plan 1 " <> cLinkSchema2)
    bob <## "contact address: connecting to contact alice"

    bob ##> ("/c " <> cLink)
    bob <## "contact address: connecting to contact alice"

testPlanAddressContactDeletedReconnected :: HasCallStack => FilePath -> IO ()
testPlanAddressContactDeletedReconnected =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True

      bob ##> ("/c " <> cLink)
      alice <#? bob
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      alice <##> bob

      bob ##> ("/_connect plan 1 " <> cLink)
      bob <## "contact address: known contact alice"
      bob <## "use @alice <message> to send messages"

      bob ##> ("/c " <> cLink)
      bob <## "contact address: known contact alice"
      bob <## "use @alice <message> to send messages"

      alice ##> "/d bob"
      alice <## "bob: contact is deleted"
      bob <## "alice (Alice) deleted contact with you"

      bob ##> ("/_connect plan 1 " <> cLink)
      bob <## "contact address: ok to connect"

      let cLinkSchema2 = linkAnotherSchema cLink
      bob ##> ("/_connect plan 1 " <> cLinkSchema2)
      bob <## "contact address: ok to connect"

      bob ##> ("/c " <> cLink)
      bob <## "connection request sent!"
      alice <## "bob (Bob) wants to connect to you!"
      alice <## "to accept: /ac bob"
      alice <## "to reject: /rc bob (the sender will NOT be notified)"
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice_1 (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")

      alice #> "@bob hi"
      bob <# "alice_1> hi"
      bob #> "@alice_1 hey"
      alice <# "bob> hey"

      bob ##> ("/_connect plan 1 " <> cLink)
      bob <## "contact address: known contact alice_1"
      bob <## "use @alice_1 <message> to send messages"

      bob ##> ("/_connect plan 1 " <> cLinkSchema2)
      bob <## "contact address: known contact alice_1"
      bob <## "use @alice_1 <message> to send messages"

      bob ##> ("/c " <> cLink)
      bob <## "contact address: known contact alice_1"
      bob <## "use @alice_1 <message> to send messages"

testPlanAddressContactViaAddress :: HasCallStack => FilePath -> IO ()
testPlanAddressContactViaAddress =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      alice ##> "/ad"
      cLink <- getContactLink alice True

      alice ##> "/pa on" -- not necessary, without it bob would receive profile update removing contact link
      alice <## "new contact address set"

      case A.parseOnly strP (B.pack cLink) of
        Left _ -> error "error parsing contact link"
        Right cReq -> do
          let profile = aliceProfile {contactLink = Just cReq}
          void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
          bob @@@ [("@alice", "")]

          bob ##> "/delete @alice"
          bob <## "alice: contact is deleted"

          void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
          bob @@@ [("@alice", "")]

          bob ##> ("/_connect plan 1 " <> cLink)
          bob <## "contact address: known contact without connection alice"

          let cLinkSchema2 = linkAnotherSchema cLink
          bob ##> ("/_connect plan 1 " <> cLinkSchema2)
          bob <## "contact address: known contact without connection alice"

          -- terminal api
          bob ##> ("/c " <> cLink)
          connecting alice bob

          bob ##> "/_delete @2 notify=off"
          bob <## "alice: contact is deleted"
          alice ##> "/_delete @2 notify=off"
          alice <## "bob: contact is deleted"

          void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
          bob @@@ [("@alice", "")]

          -- GUI api
          bob ##> "/_connect contact 1 2"
          connecting alice bob
  where
    connecting alice bob = do
      bob <## "connection request sent!"
      alice <## "bob (Bob) wants to connect to you!"
      alice <## "to accept: /ac bob"
      alice <## "to reject: /rc bob (the sender will NOT be notified)"
      alice ##> "/ac bob"
      alice <## "bob (Bob): accepting contact request, you can send messages to contact"
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")

      alice <##> bob
      bob @@@ [("@alice", "hey")]

testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO ()
testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/connect incognito"
    inv <- getInvitation alice
    bob ##> ("/connect incognito " <> inv)
    bob <## "confirmation sent!"
    bobIncognito <- getTermLine bob
    aliceIncognito <- getTermLine alice
    concurrentlyN_
      [ do
          bob <## (aliceIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
          bob <## ("use /i " <> aliceIncognito <> " to print out this incognito profile again"),
        do
          alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
      ]
    alice ?#> ("@" <> bobIncognito <> " psst, I'm incognito")
    bob ?<# (aliceIncognito <> "> psst, I'm incognito")
    bob ?#> ("@" <> aliceIncognito <> " <whispering> me too")
    alice ?<# (bobIncognito <> "> <whispering> me too")
    -- new contact is connected non incognito
    connectUsers alice cath
    alice <##> cath
    -- bob is not notified on profile change
    alice ##> "/p alice"
    concurrentlyN_
      [ alice <## "user full name removed (your 1 contacts are notified)",
        cath <## "contact alice removed full name"
      ]
    alice ?#> ("@" <> bobIncognito <> " do you see that I've changed profile?")
    bob ?<# (aliceIncognito <> "> do you see that I've changed profile?")
    bob ?#> ("@" <> aliceIncognito <> " no")
    alice ?<# (bobIncognito <> "> no")
    alice ##> "/_set prefs @2 {}"
    alice <## ("your preferences for " <> bobIncognito <> " did not change")
    (bob </)
    alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
    alice <## ("you updated preferences for " <> bobIncognito <> ":")
    alice <## "Full deletion: enabled for contact (you allow: always, contact allows: no)"
    bob <## (aliceIncognito <> " updated preferences for you:")
    bob <## "Full deletion: enabled for you (you allow: no, contact allows: always)"
    bob ##> "/_set prefs @2 {}"
    bob <## ("your preferences for " <> aliceIncognito <> " did not change")
    (alice </)
    alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"no\"}}"
    alice <## ("you updated preferences for " <> bobIncognito <> ":")
    alice <## "Full deletion: off (you allow: no, contact allows: no)"
    bob <## (aliceIncognito <> " updated preferences for you:")
    bob <## "Full deletion: off (you allow: no, contact allows: no)"
    -- list contacts
    alice ##> "/contacts"
    alice
      <### [ ConsoleString $ "i " <> bobIncognito,
             "cath (Catherine)"
           ]
    alice `hasContactProfiles` ["alice", T.pack aliceIncognito, T.pack bobIncognito, "cath"]
    bob ##> "/contacts"
    bob <## ("i " <> aliceIncognito)
    bob `hasContactProfiles` ["bob", T.pack aliceIncognito, T.pack bobIncognito]
    -- alice deletes contact, incognito profile is deleted
    alice ##> ("/d " <> bobIncognito)
    alice <## (bobIncognito <> ": contact is deleted")
    bob <## (aliceIncognito <> " deleted contact with you")
    alice ##> "/contacts"
    alice <## "cath (Catherine)"
    alice `hasContactProfiles` ["alice", "cath"]
    -- bob deletes contact, incognito profile is deleted
    bob ##> ("/d " <> aliceIncognito)
    bob <## (aliceIncognito <> ": contact is deleted")
    bob ##> "/contacts"
    (bob </)
    bob `hasContactProfiles` ["bob"]

testConnectIncognitoContactAddress :: HasCallStack => FilePath -> IO ()
testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True
    bob ##> ("/c i " <> cLink)
    bobIncognito <- getTermLine bob
    bob <## "connection request sent incognito!"
    alice <## (bobIncognito <> " wants to connect to you!")
    alice <## ("to accept: /ac " <> bobIncognito)
    alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)")
    alice ##> ("/ac " <> bobIncognito)
    alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact")
    _ <- getTermLine bob
    concurrentlyN_
      [ do
          bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
          bob <## "use /i alice to print out this incognito profile again",
        alice <## (bobIncognito <> ": contact is connected")
      ]
    -- conversation is incognito
    alice #> ("@" <> bobIncognito <> " who are you?")
    bob ?<# "alice> who are you?"
    bob ?#> "@alice I'm Batman"
    alice <# (bobIncognito <> "> I'm Batman")
    -- list contacts
    bob ##> "/contacts"
    bob <## "i alice (Alice)"
    bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
    threadDelay 500000
    -- delete contact, incognito profile is deleted
    bob ##> "/d alice"
    bob <## "alice: contact is deleted"
    alice <## (bobIncognito <> " deleted contact with you")
    bob ##> "/contacts"
    (bob </)
    bob `hasContactProfiles` ["bob"]

testAcceptContactRequestIncognito :: HasCallStack => FilePath -> IO ()
testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/ad"
    cLink <- getContactLink alice True
    bob ##> ("/c " <> cLink)
    alice <#? bob
    alice ##> "/accept incognito bob"
    alice <## "bob (Bob): accepting contact request, you can send messages to contact"
    aliceIncognitoBob <- getTermLine alice
    concurrentlyN_
      [ bob <## (aliceIncognitoBob <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob)
          alice <## "use /i bob to print out this incognito profile again"
      ]
    -- conversation is incognito
    alice ?#> "@bob my profile is totally inconspicuous"
    bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous")
    bob #> ("@" <> aliceIncognitoBob <> " I know!")
    alice ?<# "bob> I know!"
    -- list contacts
    alice ##> "/contacts"
    alice <## "i bob (Bob)"
    alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob]
    -- delete contact, incognito profile is deleted
    alice ##> "/d bob"
    alice <## "bob: contact is deleted"
    bob <## (aliceIncognitoBob <> " deleted contact with you")
    alice ##> "/contacts"
    (alice </)
    alice `hasContactProfiles` ["alice"]
    -- /_accept api
    cath ##> ("/c " <> cLink)
    alice <#? cath
    alice ##> "/_accept incognito=on 1"
    alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
    aliceIncognitoCath <- getTermLine alice
    concurrentlyN_
      [ cath <## (aliceIncognitoCath <> ": contact is connected"),
        do
          alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath)
          alice <## "use /i cath to print out this incognito profile again"
      ]
    alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath]
    cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath]

testSetConnectionIncognito :: HasCallStack => FilePath -> IO ()
testSetConnectionIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/connect"
    inv <- getInvitation alice
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    bob ##> ("/connect " <> inv)
    bob <## "confirmation sent!"
    aliceIncognito <- getTermLine alice
    concurrentlyN_
      [ bob <## (aliceIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## ("use /i bob to print out this incognito profile again")
      ]
    alice ?#> ("@bob hi")
    bob <# (aliceIncognito <> "> hi")
    bob #> ("@" <> aliceIncognito <> " hey")
    alice ?<# ("bob> hey")
    alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito]
    bob `hasContactProfiles` ["bob", T.pack aliceIncognito]

testResetConnectionIncognito :: HasCallStack => FilePath -> IO ()
testResetConnectionIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/_connect 1 incognito=on"
    inv <- getInvitation alice
    alice ##> "/_set incognito :1 off"
    alice <## "connection 1 changed to non incognito"
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (bob <## "alice (Alice): contact is connected")
      (alice <## "bob (Bob): contact is connected")
    alice <##> bob
    alice `hasContactProfiles` ["alice", "bob"]
    bob `hasContactProfiles` ["alice", "bob"]

testSetConnectionIncognitoProhibitedDuringNegotiation :: HasCallStack => FilePath -> IO ()
testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do
  inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
    threadDelay 250000
    alice ##> "/connect"
    getInvitation alice
  withNewTestChat tmp "bob" bobProfile $ \bob -> do
    threadDelay 250000
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
  withTestChat tmp "alice" $ \alice -> do
    threadDelay 250000
    alice <## "bob (Bob): contact is connected"
    alice ##> "/_set incognito :1 on"
    alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}"
    withTestChat tmp "bob" $ \bob -> do
      bob <## "alice (Alice): contact is connected"
      alice <##> bob
      alice `hasContactProfiles` ["alice", "bob"]
      bob `hasContactProfiles` ["alice", "bob"]

testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => FilePath -> IO ()
testSetConnectionIncognitoProhibitedDuringNegotiationSlow tmp = do
  inv <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do
    threadDelay 250000
    alice ##> "/connect"
    getInvitation alice
  withNewTestChatCfg tmp testCfgSlow "bob" bobProfile $ \bob -> do
    threadDelay 250000
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
  withTestChatCfg tmp testCfgSlow "alice" $ \alice -> do
    threadDelay 250000
    alice ##> "/_set incognito :1 on"
    alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}"
    withTestChatCfg tmp testCfgSlow "bob" $ \bob -> do
      concurrently_
        (bob <## "alice (Alice): contact is connected")
        (alice <## "bob (Bob): contact is connected")
      alice <##> bob
      alice `hasContactProfiles` ["alice", "bob"]
      bob `hasContactProfiles` ["alice", "bob"]

testConnectionIncognitoUnchangedErrors :: HasCallStack => FilePath -> IO ()
testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/connect"
    inv <- getInvitation alice
    alice ##> "/_set incognito :1 off"
    alice <## "incognito mode change prohibited"
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    alice ##> "/_set incognito :1 on"
    alice <## "incognito mode change prohibited"
    alice ##> "/_set incognito :1 off"
    alice <## "connection 1 changed to non incognito"
    alice ##> "/_set incognito :1 off"
    alice <## "incognito mode change prohibited"
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (bob <## "alice (Alice): contact is connected")
      (alice <## "bob (Bob): contact is connected")
    alice <##> bob
    alice `hasContactProfiles` ["alice", "bob"]
    bob `hasContactProfiles` ["alice", "bob"]

testSetResetSetConnectionIncognito :: HasCallStack => FilePath -> IO ()
testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/_connect 1 incognito=off"
    inv <- getInvitation alice
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    alice ##> "/_set incognito :1 off"
    alice <## "connection 1 changed to non incognito"
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    bob ##> ("/_connect 1 incognito=off " <> inv)
    bob <## "confirmation sent!"
    aliceIncognito <- getTermLine alice
    concurrentlyN_
      [ bob <## (aliceIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## ("use /i bob to print out this incognito profile again")
      ]
    alice ?#> ("@bob hi")
    bob <# (aliceIncognito <> "> hi")
    bob #> ("@" <> aliceIncognito <> " hey")
    alice ?<# ("bob> hey")
    alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito]
    bob `hasContactProfiles` ["bob", T.pack aliceIncognito]

testJoinGroupIncognito :: HasCallStack => FilePath -> IO ()
testJoinGroupIncognito =
  testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
    \alice bob cath dan -> do
      -- non incognito connections
      connectUsers alice bob
      connectUsers alice dan
      connectUsers bob cath
      connectUsers bob dan
      connectUsers cath dan
      -- cath connected incognito to alice
      alice ##> "/c"
      inv <- getInvitation alice
      cath ##> ("/c i " <> inv)
      cath <## "confirmation sent!"
      cathIncognito <- getTermLine cath
      concurrentlyN_
        [ do
            cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
            cath <## "use /i alice to print out this incognito profile again",
          alice <## (cathIncognito <> ": contact is connected")
        ]
      -- alice creates group
      alice ##> "/g secret_club"
      alice <## "group #secret_club is created"
      alice <## "to add members use /a secret_club <name> or /create link #secret_club"
      -- alice invites bob
      alice ##> "/a secret_club bob admin"
      concurrentlyN_
        [ alice <## "invitation to join the group #secret_club sent to bob",
          do
            bob <## "#secret_club: alice invites you to join the group as admin"
            bob <## "use /j secret_club to accept"
        ]
      bob ##> "/j secret_club"
      concurrently_
        (alice <## "#secret_club: bob joined the group")
        (bob <## "#secret_club: you joined the group")
      -- alice invites cath
      alice ##> ("/a secret_club " <> cathIncognito <> " admin")
      concurrentlyN_
        [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
          do
            cath <## "#secret_club: alice invites you to join the group as admin"
            cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
        ]
      -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
      cath ##> "/j secret_club"
      concurrentlyN_
        [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
          do
            cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
            cath <## "#secret_club: member bob_1 (Bob) is connected",
          do
            bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
            bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
        ]
      -- cath cannot invite to the group because her membership is incognito
      cath ##> "/a secret_club dan"
      cath <## "you are using an incognito profile for this group - prohibited to invite contacts"
      -- alice invites dan
      alice ##> "/a secret_club dan admin"
      concurrentlyN_
        [ alice <## "invitation to join the group #secret_club sent to dan",
          do
            dan <## "#secret_club: alice invites you to join the group as admin"
            dan <## "use /j secret_club to accept"
        ]
      dan ##> "/j secret_club"
      -- cath and dan don't merge contacts
      concurrentlyN_
        [ alice <## "#secret_club: dan joined the group",
          do
            dan <## "#secret_club: you joined the group"
            dan
              <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
                     "#secret_club: member bob_1 (Bob) is connected",
                     "contact bob_1 is merged into bob",
                     "use @bob <message> to send messages"
                   ],
          do
            bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
            bob <## "#secret_club: new member dan_1 is connected"
            bob <## "contact dan_1 is merged into dan"
            bob <## "use @dan <message> to send messages",
          do
            cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
            cath <## "#secret_club: new member dan_1 is connected"
        ]
      -- send messages - group is incognito for cath
      alice #> "#secret_club hello"
      concurrentlyN_
        [ bob <# "#secret_club alice> hello",
          cath ?<# "#secret_club alice> hello",
          dan <# "#secret_club alice> hello"
        ]
      bob #> "#secret_club hi there"
      concurrentlyN_
        [ alice <# "#secret_club bob> hi there",
          cath ?<# "#secret_club bob_1> hi there",
          dan <# "#secret_club bob> hi there"
        ]
      cath ?#> "#secret_club hey"
      concurrentlyN_
        [ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
          bob <# ("#secret_club " <> cathIncognito <> "> hey"),
          dan <# ("#secret_club " <> cathIncognito <> "> hey")
        ]
      dan #> "#secret_club how is it going?"
      concurrentlyN_
        [ alice <# "#secret_club dan> how is it going?",
          bob <# "#secret_club dan> how is it going?",
          cath ?<# "#secret_club dan_1> how is it going?"
        ]
      -- cath and bob can send messages via new direct connection, cath is incognito
      bob #> ("@" <> cathIncognito <> " hi, I'm bob")
      cath ?<# "bob_1> hi, I'm bob"
      cath ?#> "@bob_1 hey, I'm incognito"
      bob <# (cathIncognito <> "> hey, I'm incognito")
      -- cath and dan can send messages via new direct connection, cath is incognito
      dan #> ("@" <> cathIncognito <> " hi, I'm dan")
      cath ?<# "dan_1> hi, I'm dan"
      cath ?#> "@dan_1 hey, I'm incognito"
      dan <# (cathIncognito <> "> hey, I'm incognito")
      -- non incognito connections are separate
      bob <##> cath
      dan <##> cath
      -- list groups
      cath ##> "/gs"
      cath <## "i #secret_club (4 members)"
      -- list group members
      alice ##> "/ms secret_club"
      alice
        <### [ "alice (Alice): owner, you, created group",
               "bob (Bob): admin, invited, connected",
               ConsoleString $ cathIncognito <> ": admin, invited, connected",
               "dan (Daniel): admin, invited, connected"
             ]
      bob ##> "/ms secret_club"
      bob
        <### [ "alice (Alice): owner, host, connected",
               "bob (Bob): admin, you, connected",
               ConsoleString $ cathIncognito <> ": admin, connected",
               "dan (Daniel): admin, connected"
             ]
      cath ##> "/ms secret_club"
      cath
        <### [ "alice (Alice): owner, host, connected",
               "bob_1 (Bob): admin, connected",
               ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
               "dan_1 (Daniel): admin, connected"
             ]
      dan ##> "/ms secret_club"
      dan
        <### [ "alice (Alice): owner, host, connected",
               "bob (Bob): admin, connected",
               ConsoleString $ cathIncognito <> ": admin, connected",
               "dan (Daniel): admin, you, connected"
             ]
      -- remove member
      bob ##> ("/rm secret_club " <> cathIncognito)
      concurrentlyN_
        [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
          alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
          dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
          do
            cath <## "#secret_club: bob_1 removed you from the group"
            cath <## "use /d #secret_club to delete the group"
        ]
      bob #> "#secret_club hi"
      concurrentlyN_
        [ alice <# "#secret_club bob> hi",
          dan <# "#secret_club bob> hi",
          (cath </)
        ]
      alice #> "#secret_club hello"
      concurrentlyN_
        [ bob <# "#secret_club alice> hello",
          dan <# "#secret_club alice> hello",
          (cath </)
        ]
      cath ##> "#secret_club hello"
      cath <## "you are no longer a member of the group"
      -- cath can still message members directly
      bob #> ("@" <> cathIncognito <> " I removed you from group")
      cath ?<# "bob_1> I removed you from group"
      cath ?#> "@bob_1 ok"
      bob <# (cathIncognito <> "> ok")

testCantInviteContactIncognito :: HasCallStack => FilePath -> IO ()
testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- alice connected incognito to bob
    alice ##> "/c i"
    inv <- getInvitation alice
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
    aliceIncognito <- getTermLine alice
    concurrentlyN_
      [ bob <## (aliceIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## "use /i bob to print out this incognito profile again"
      ]
    -- alice creates group non incognito
    alice ##> "/g club"
    alice <## "group #club is created"
    alice <## "to add members use /a club <name> or /create link #club"
    alice ##> "/a club bob"
    alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"
    -- bob doesn't receive invitation
    (bob </)

testCantSeeGlobalPrefsUpdateIncognito :: HasCallStack => FilePath -> IO ()
testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    alice ##> "/c i"
    invIncognito <- getInvitation alice
    alice ##> "/c"
    inv <- getInvitation alice
    bob ##> ("/c " <> invIncognito)
    bob <## "confirmation sent!"
    aliceIncognito <- getTermLine alice
    cath ##> ("/c " <> inv)
    cath <## "confirmation sent!"
    concurrentlyN_
      [ bob <## (aliceIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
          alice <## "use /i bob to print out this incognito profile again",
        do
          cath <## "alice (Alice): contact is connected"
      ]
    alice <## "cath (Catherine): contact is connected"
    alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
    alice <## "user full name removed (your 1 contacts are notified)"
    alice <## "updated preferences:"
    alice <## "Full deletion allowed: always"
    (alice </)
    -- bob doesn't receive profile update
    (bob </)
    cath <## "contact alice removed full name"
    cath <## "alice updated preferences for you:"
    cath <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
    (cath </)
    bob ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
    bob <## ("you updated preferences for " <> aliceIncognito <> ":")
    bob <## "Full deletion: enabled for contact (you allow: always, contact allows: no)"
    alice <## "bob updated preferences for you:"
    alice <## "Full deletion: enabled for you (you allow: no, contact allows: always)"
    alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"yes\"}}"
    alice <## "you updated preferences for bob:"
    alice <## "Full deletion: enabled (you allow: yes, contact allows: always)"
    bob <## (aliceIncognito <> " updated preferences for you:")
    bob <## "Full deletion: enabled (you allow: always, contact allows: yes)"
    (cath </)
    alice ##> "/_set prefs @3 {\"fullDelete\": {\"allow\": \"always\"}}"
    alice <## "your preferences for cath did not change"
    alice ##> "/_set prefs @3 {\"fullDelete\": {\"allow\": \"yes\"}}"
    alice <## "you updated preferences for cath:"
    alice <## "Full deletion: off (you allow: yes, contact allows: no)"
    cath <## "alice updated preferences for you:"
    cath <## "Full deletion: off (you allow: default (no), contact allows: yes)"

testDeleteContactThenGroupDeletesIncognitoProfile :: HasCallStack => FilePath -> IO ()
testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- bob connects incognito to alice
    alice ##> "/c"
    inv <- getInvitation alice
    bob ##> ("/c i " <> inv)
    bob <## "confirmation sent!"
    bobIncognito <- getTermLine bob
    concurrentlyN_
      [ alice <## (bobIncognito <> ": contact is connected"),
        do
          bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
          bob <## "use /i alice to print out this incognito profile again"
      ]
    -- bob joins group using incognito profile
    alice ##> "/g team"
    alice <## "group #team is created"
    alice <## "to add members use /a team <name> or /create link #team"
    alice ##> ("/a team " <> bobIncognito)
    concurrentlyN_
      [ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
        do
          bob <## "#team: alice invites you to join the group as member"
          bob <## ("use /j team to join incognito as " <> bobIncognito)
      ]
    bob ##> "/j team"
    concurrently_
      (alice <## ("#team: " <> bobIncognito <> " joined the group"))
      (bob <## ("#team: you joined the group incognito as " <> bobIncognito))
    bob ##> "/contacts"
    bob <## "i alice (Alice)"
    bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
    -- delete contact
    bob ##> "/d alice"
    bob <## "alice: contact is deleted"
    alice <## (bobIncognito <> " deleted contact with you")
    bob ##> "/contacts"
    (bob </)
    bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
    -- delete group
    bob ##> "/l team"
    concurrentlyN_
      [ do
          bob <## "#team: you left the group"
          bob <## "use /d #team to delete the group",
        alice <## ("#team: " <> bobIncognito <> " left the group")
      ]
    bob ##> "/d #team"
    bob <## "#team: you deleted the group"
    bob `hasContactProfiles` ["bob"]

testDeleteGroupThenContactDeletesIncognitoProfile :: HasCallStack => FilePath -> IO ()
testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- bob connects incognito to alice
    alice ##> "/c"
    inv <- getInvitation alice
    bob ##> ("/c i " <> inv)
    bob <## "confirmation sent!"
    bobIncognito <- getTermLine bob
    concurrentlyN_
      [ alice <## (bobIncognito <> ": contact is connected"),
        do
          bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
          bob <## "use /i alice to print out this incognito profile again"
      ]
    -- bob joins group using incognito profile
    alice ##> "/g team"
    alice <## "group #team is created"
    alice <## "to add members use /a team <name> or /create link #team"
    alice ##> ("/a team " <> bobIncognito)
    concurrentlyN_
      [ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
        do
          bob <## "#team: alice invites you to join the group as member"
          bob <## ("use /j team to join incognito as " <> bobIncognito)
      ]
    bob ##> "/j team"
    concurrently_
      (alice <## ("#team: " <> bobIncognito <> " joined the group"))
      (bob <## ("#team: you joined the group incognito as " <> bobIncognito))
    bob ##> "/contacts"
    bob <## "i alice (Alice)"
    bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
    -- delete group
    bob ##> "/l team"
    concurrentlyN_
      [ do
          bob <## "#team: you left the group"
          bob <## "use /d #team to delete the group",
        alice <## ("#team: " <> bobIncognito <> " left the group")
      ]
    bob ##> "/d #team"
    bob <## "#team: you deleted the group"
    bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
    -- delete contact
    bob ##> "/d alice"
    bob <## "alice: contact is deleted"
    alice <## (bobIncognito <> " deleted contact with you")
    bob ##> "/contacts"
    (bob </)
    bob `hasContactProfiles` ["bob"]

testSetAlias :: HasCallStack => FilePath -> IO ()
testSetAlias = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    connectUsers alice bob
    alice #$> ("/_set alias @2 my friend bob", id, "contact bob alias updated: my friend bob")
    alice ##> "/contacts"
    alice <## "bob (Bob) (alias: my friend bob)"
    alice #$> ("/_set alias @2", id, "contact bob alias removed")
    alice ##> "/contacts"
    alice <## "bob (Bob)"

testChangePCCUser :: HasCallStack => FilePath -> IO ()
testChangePCCUser = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- Create a new invite
    alice ##> "/connect"
    inv <- getInvitation alice
    -- Create new user and go back to original user
    alice ##> "/create user alisa"
    showActiveUser alice "alisa"
    alice ##> "/create user alisa2"
    showActiveUser alice "alisa2"
    alice ##> "/user alice"
    showActiveUser alice "alice (Alice)"
    -- Change connection to newly created user
    alice ##> "/_set conn user :1 2"
    alice <## "connection 1 changed from user alice to user alisa"
    alice ##> "/user alisa"
    showActiveUser alice "alisa"
    -- Change connection back to other user
    alice ##> "/_set conn user :1 3"
    alice <## "connection 1 changed from user alisa to user alisa2"
    alice ##> "/user alisa2"
    showActiveUser alice "alisa2"
    -- Connect
    bob ##> ("/connect " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (alice <## "bob (Bob): contact is connected")
      (bob <## "alisa2: contact is connected")

testChangePCCUserFromIncognito :: HasCallStack => FilePath -> IO ()
testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- Create a new invite and set as incognito
    alice ##> "/connect"
    inv <- getInvitation alice
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    -- Create new user and go back to original user
    alice ##> "/create user alisa"
    showActiveUser alice "alisa"
    alice ##> "/user alice"
    showActiveUser alice "alice (Alice)"
    -- Change connection to newly created user
    alice ##> "/_set conn user :1 2"
    alice <## "connection 1 changed from user alice to user alisa"
    alice `hasContactProfiles` ["alice"]
    alice ##> "/user alisa"
    showActiveUser alice "alisa"
    -- Change connection back to initial user
    alice ##> "/_set conn user :1 1"
    alice <## "connection 1 changed from user alisa to user alice"
    alice ##> "/user alice"
    showActiveUser alice "alice (Alice)"
    -- Connect
    bob ##> ("/connect " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (alice <## "bob (Bob): contact is connected")
      (bob <## "alice (Alice): contact is connected")

testChangePCCUserAndThenIncognito :: HasCallStack => FilePath -> IO ()
testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- Create a new invite and set as incognito
    alice ##> "/connect"
    inv <- getInvitation alice
    -- Create new user and go back to original user
    alice ##> "/create user alisa"
    showActiveUser alice "alisa"
    alice ##> "/user alice"
    showActiveUser alice "alice (Alice)"
    -- Change connection to newly created user
    alice ##> "/_set conn user :1 2"
    alice <## "connection 1 changed from user alice to user alisa"
    alice ##> "/user alisa"
    showActiveUser alice "alisa"
    -- Change connection to incognito and make sure it's attached to the newly created user profile
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    bob ##> ("/connect " <> inv)
    bob <## "confirmation sent!"
    alisaIncognito <- getTermLine alice
    concurrentlyN_
      [ bob <## (alisaIncognito <> ": contact is connected"),
        do
          alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito)
          alice <## ("use /i bob to print out this incognito profile again")
      ]

testChangePCCUserDiffSrv :: HasCallStack => FilePath -> IO ()
testChangePCCUserDiffSrv = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    -- Create a new invite
    alice ##> "/connect"
    _ <- getInvitation alice
    alice ##> "/_set incognito :1 on"
    alice <## "connection 1 changed to incognito"
    -- Create new user with different servers
    alice ##> "/create user alisa"
    showActiveUser alice "alisa"
    alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok")
    alice ##> "/user alice"
    showActiveUser alice "alice (Alice)"
    -- Change connection to newly created user and use the newly created connection
    alice ##> "/_set conn user :1 2"
    alice <## "connection 1 changed from user alice to user alisa, new link:"
    alice <## ""
    inv <- getTermLine alice
    alice <## ""
    alice `hasContactProfiles` ["alice"]
    alice ##> "/user alisa"
    showActiveUser alice "alisa"
    -- Connect
    bob ##> ("/connect " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (alice <## "bob (Bob): contact is connected")
      (bob <## "alisa: contact is connected")

testSetConnectionAlias :: HasCallStack => FilePath -> IO ()
testSetConnectionAlias = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    alice ##> "/c"
    inv <- getInvitation alice
    alice @@@ [(":1", "")]
    alice ##> "/_set alias :1 friend"
    alice <## "connection 1 alias updated: friend"
    bob ##> ("/c " <> inv)
    bob <## "confirmation sent!"
    concurrently_
      (alice <## "bob (Bob): contact is connected")
      (bob <## "alice (Alice): contact is connected")
    threadDelay 100000
    alice @@@ [("@bob", lastChatFeature)]
    alice ##> "/contacts"
    alice <## "bob (Bob) (alias: friend)"

testSetContactPrefs :: HasCallStack => FilePath -> IO ()
testSetContactPrefs = testChat2 aliceProfile bobProfile $
  \alice bob -> withXFTPServer $ do
    alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok")
    bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok")
    createDirectoryIfMissing True "./tests/tmp/alice"
    createDirectoryIfMissing True "./tests/tmp/bob"
    copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test.txt"
    copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test.txt"
    bob ##> "/_profile 1 {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
    bob <## "profile image removed"
    bob <## "updated preferences:"
    bob <## "Voice messages allowed: no"
    (bob </)
    connectUsers alice bob
    alice ##> "/_set prefs @2 {}"
    alice <## "your preferences for bob did not change"
    (bob </)
    let startFeatures = [(0, e2eeInfoPQStr), (0, "Disappearing messages: allowed"), (0, "Full deletion: off"), (0, "Message reactions: enabled"), (0, "Voice messages: off"), (0, "Audio/video calls: enabled")]
    alice #$> ("/_get chat @2 count=100", chat, startFeatures)
    bob #$> ("/_get chat @2 count=100", chat, startFeatures)
    let sendVoice = "/_send @2 json {\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}"
        voiceNotAllowed = "bad chat command: feature not allowed Voice messages"
    alice ##> sendVoice
    alice <## voiceNotAllowed
    bob ##> sendVoice
    bob <## voiceNotAllowed
    -- alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"always\"}}"
    alice ##> "/set voice @bob always"
    alice <## "you updated preferences for bob:"
    alice <## "Voice messages: enabled for contact (you allow: always, contact allows: no)"
    alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact")])
    bob <## "alice updated preferences for you:"
    bob <## "Voice messages: enabled for you (you allow: default (no), contact allows: always)"
    bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")])
    alice ##> sendVoice
    alice <## voiceNotAllowed

    -- sending voice message allowed
    bob ##> sendVoice
    bob <# "@alice voice message (00:10)"
    bob <# "/f @alice test.txt"
    bob <## "use /fc 1 to cancel sending"
    alice <# "bob> voice message (00:10)"
    alice <# "bob> sends file test.txt (11 bytes / 11 bytes)"
    alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
    bob <## "completed uploading file 1 (test.txt) for alice"
    alice ##> "/fr 1"
    alice
      <### [ "saving file 1 from bob to test_1.txt",
             "started receiving file 1 (test.txt) from bob"
           ]
    alice <## "completed receiving file 1 (test.txt) from bob"
    (bob </)

    -- alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
    alice ##> "/set voice no"
    alice <## "updated preferences:"
    alice <## "Voice messages allowed: no"
    (alice </)
    alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"yes\"}}"
    alice <## "you updated preferences for bob:"
    alice <## "Voice messages: off (you allow: yes, contact allows: no)"
    alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off")])
    bob <## "alice updated preferences for you:"
    bob <## "Voice messages: off (you allow: default (no), contact allows: yes)"
    bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off")])
    (bob </)
    bob ##> "/_profile 1 {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"yes\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
    bob <## "user full name removed (your 1 contacts are notified)"
    bob <## "updated preferences:"
    bob <## "Voice messages allowed: yes"
    bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled")])
    (bob </)
    alice <## "contact bob removed full name"
    alice <## "bob updated preferences for you:"
    alice <## "Voice messages: enabled (you allow: yes, contact allows: yes)"
    alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "updated profile"), (0, "Voice messages: enabled")])
    (alice </)
    bob ##> "/_set prefs @2 {}"
    bob <## "your preferences for alice did not change"
    -- no change
    bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled")])
    (bob </)
    (alice </)
    alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"no\"}}"
    alice <## "you updated preferences for bob:"
    alice <## "Voice messages: off (you allow: no, contact allows: yes)"
    alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "updated profile"), (0, "Voice messages: enabled"), (1, "Voice messages: off")])
    bob <## "alice updated preferences for you:"
    bob <## "Voice messages: off (you allow: default (yes), contact allows: no)"
    bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled"), (0, "Voice messages: off")])

testFeatureOffers :: HasCallStack => FilePath -> IO ()
testFeatureOffers = testChat2 aliceProfile bobProfile $
  \alice bob -> do
    connectUsers alice bob
    alice ##> "/set delete @bob yes"
    alice <## "you updated preferences for bob:"
    alice <## "Full deletion: off (you allow: yes, contact allows: no)"
    alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "you offered Full deletion")])
    bob <## "alice updated preferences for you:"
    bob <## "Full deletion: off (you allow: default (no), contact allows: yes)"
    bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "offered Full deletion")])
    alice ##> "/set delete @bob no"
    alice <## "you updated preferences for bob:"
    alice <## "Full deletion: off (you allow: no, contact allows: no)"
    alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "you offered Full deletion"), (1, "you cancelled Full deletion")])
    bob <## "alice updated preferences for you:"
    bob <## "Full deletion: off (you allow: default (no), contact allows: no)"
    bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "offered Full deletion"), (0, "cancelled Full deletion")])

testUpdateGroupPrefs :: HasCallStack => FilePath -> IO ()
testUpdateGroupPrefs =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      createGroup2 "team" alice bob
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected")])
      threadDelay 500000
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")])
      alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}"
      alice <## "updated group preferences:"
      alice <## "Full deletion: on"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on")])
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Full deletion: on"
      threadDelay 500000
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")])
      alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}"
      alice <## "updated group preferences:"
      alice <## "Full deletion: off"
      alice <## "Voice messages: off"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")])
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Full deletion: off"
      bob <## "Voice messages: off"
      threadDelay 500000
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")])
      alice ##> "/set voice #team on"
      alice <## "updated group preferences:"
      alice <## "Voice messages: on"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Voice messages: on"
      threadDelay 500000
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")])
      threadDelay 500000
      alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}"
      -- no update
      threadDelay 500000
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
      alice #> "#team hey"
      bob <# "#team alice> hey"
      threadDelay 1000000
      bob #> "#team hi"
      alice <# "#team bob> hi"
      threadDelay 500000
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on"), (0, "hey"), (1, "hi")])

testAllowFullDeletionContact :: HasCallStack => FilePath -> IO ()
testAllowFullDeletionContact =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      connectUsers alice bob
      alice <##> bob
      alice ##> "/set delete @bob always"
      alice <## "you updated preferences for bob:"
      alice <## "Full deletion: enabled for contact (you allow: always, contact allows: no)"
      bob <## "alice updated preferences for you:"
      bob <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "Full deletion: enabled for contact")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "Full deletion: enabled for you")])
      bob #$> ("/_delete item @2 " <> itemId 2 <> " broadcast", id, "message deleted")
      alice <# "bob> [deleted] hey"
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (1, "Full deletion: enabled for contact")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (0, "Full deletion: enabled for you")])

testAllowFullDeletionGroup :: HasCallStack => FilePath -> IO ()
testAllowFullDeletionGroup =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      createGroup2 "team" alice bob
      threadDelay 1500000
      alice #> "#team hi"
      bob <# "#team alice> hi"
      threadDelay 1000000
      bob #> "#team hey"
      bob ##> "/last_item_id #team"
      msgItemId <- getTermLine bob
      alice <# "#team bob> hey"
      alice ##> "/set delete #team on"
      alice <## "updated group preferences:"
      alice <## "Full deletion: on"
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Full deletion: on"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (1, "hey"), (0, "Full deletion: on")])
      bob #$> ("/_delete item #1 " <> msgItemId <> " broadcast", id, "message deleted")
      alice <# "#team bob> [deleted] hey"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "hi"), (1, "Full deletion: on")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (0, "Full deletion: on")])

testProhibitDirectMessages :: HasCallStack => FilePath -> IO ()
testProhibitDirectMessages =
  testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
    \alice bob cath dan -> do
      createGroup3 "team" alice bob cath
      threadDelay 1000000
      alice ##> "/set direct #team off"
      alice <## "updated group preferences:"
      alice <## "Direct messages: off"
      directProhibited bob
      directProhibited cath
      threadDelay 1000000
      -- still can send direct messages to direct contacts
      alice #> "@bob hello again"
      bob <# "alice> hello again"
      alice #> "@cath hello again"
      cath <# "alice> hello again"
      bob ##> "@cath hello again"
      bob <## "direct messages to indirect contact cath are prohibited"
      (cath </)
      connectUsers cath dan
      addMember "team" cath dan GRMember
      dan ##> "/j #team"
      concurrentlyN_
        [ cath <## "#team: dan joined the group",
          do
            dan <## "#team: you joined the group"
            dan
              <### [ "#team: member alice (Alice) is connected",
                     "#team: member bob (Bob) is connected"
                   ],
          do
            alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
            alice <## "#team: new member dan is connected",
          do
            bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
            bob <## "#team: new member dan is connected"
        ]
      alice ##> "@dan hi"
      alice <## "direct messages to indirect contact dan are prohibited"
      bob ##> "@dan hi"
      bob <## "direct messages to indirect contact dan are prohibited"
      (dan </)
      dan ##> "@alice hi"
      dan <## "direct messages to indirect contact alice are prohibited"
      dan ##> "@bob hi"
      dan <## "direct messages to indirect contact bob are prohibited"
      dan #> "@cath hi"
      cath <# "dan> hi"
      cath #> "@dan hi"
      dan <# "cath> hi"
  where
    directProhibited :: HasCallStack => TestCC -> IO ()
    directProhibited cc = do
      cc <## "alice updated group #team:"
      cc <## "updated group preferences:"
      cc <## "Direct messages: off"

testEnableTimedMessagesContact :: HasCallStack => FilePath -> IO ()
testEnableTimedMessagesContact =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      connectUsers alice bob
      alice ##> "/_set prefs @2 {\"timedMessages\": {\"allow\": \"yes\", \"ttl\": 1}}"
      alice <## "you updated preferences for bob:"
      alice <## "Disappearing messages: enabled (you allow: yes (1 sec), contact allows: yes)"
      bob <## "alice updated preferences for you:"
      bob <## "Disappearing messages: enabled (you allow: yes (1 sec), contact allows: yes (1 sec))"
      bob ##> "/set disappear @alice yes"
      bob <## "your preferences for alice did not change"
      alice <##> bob
      threadDelay 500000
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")])
      threadDelay 1000000
      alice <## "timed message deleted: hi"
      alice <## "timed message deleted: hey"
      bob <## "timed message deleted: hi"
      bob <## "timed message deleted: hey"
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
      -- turn off, messages are not disappearing
      bob ##> "/set disappear @alice no"
      bob <## "you updated preferences for alice:"
      bob <## "Disappearing messages: off (you allow: no, contact allows: yes (1 sec))"
      alice <## "bob updated preferences for you:"
      alice <## "Disappearing messages: off (you allow: yes (1 sec), contact allows: no)"
      alice <##> bob
      threadDelay 1500000
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (0, "Disappearing messages: off"), (1, "hi"), (0, "hey")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (1, "Disappearing messages: off"), (0, "hi"), (1, "hey")])
      -- test api
      bob ##> "/set disappear @alice yes 30s"
      bob <## "you updated preferences for alice:"
      bob <## "Disappearing messages: enabled (you allow: yes (30 sec), contact allows: yes (1 sec))"
      alice <## "bob updated preferences for you:"
      alice <## "Disappearing messages: enabled (you allow: yes (30 sec), contact allows: yes (30 sec))"
      bob ##> "/set disappear @alice week" -- "yes" is optional
      bob <## "you updated preferences for alice:"
      bob <## "Disappearing messages: enabled (you allow: yes (1 week), contact allows: yes (1 sec))"
      alice <## "bob updated preferences for you:"
      alice <## "Disappearing messages: enabled (you allow: yes (1 week), contact allows: yes (1 week))"

testEnableTimedMessagesGroup :: HasCallStack => FilePath -> IO ()
testEnableTimedMessagesGroup =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      createGroup2 "team" alice bob
      threadDelay 1000000
      alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}"
      alice <## "updated group preferences:"
      alice <## "Disappearing messages: on (1 sec)"
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Disappearing messages: on (1 sec)"
      threadDelay 1000000
      alice #> "#team hi"
      bob <# "#team alice> hi"
      threadDelay 500000
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "hi")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "hi")])
      threadDelay 1000000
      alice <## "timed message deleted: hi"
      bob <## "timed message deleted: hi"
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)")])
      -- turn off, messages are not disappearing
      alice ##> "/set disappear #team off"
      alice <## "updated group preferences:"
      alice <## "Disappearing messages: off"
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Disappearing messages: off"
      threadDelay 1000000
      alice #> "#team hey"
      bob <# "#team alice> hey"
      threadDelay 1500000
      alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "Disappearing messages: off"), (1, "hey")])
      bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "Disappearing messages: off"), (0, "hey")])
      -- test api
      alice ##> "/set disappear #team on 30s"
      alice <## "updated group preferences:"
      alice <## "Disappearing messages: on (30 sec)"
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Disappearing messages: on (30 sec)"
      alice ##> "/set disappear #team week" -- "on" is optional
      alice <## "updated group preferences:"
      alice <## "Disappearing messages: on (1 week)"
      bob <## "alice updated group #team:"
      bob <## "updated group preferences:"
      bob <## "Disappearing messages: on (1 week)"

testTimedMessagesEnabledGlobally :: HasCallStack => FilePath -> IO ()
testTimedMessagesEnabledGlobally =
  testChat2 aliceProfile bobProfile $
    \alice bob -> do
      alice ##> "/set disappear yes"
      alice <## "user profile did not change"
      connectUsers alice bob
      bob ##> "/_set prefs @2 {\"timedMessages\": {\"allow\": \"yes\", \"ttl\": 1}}"
      bob <## "you updated preferences for alice:"
      bob <## "Disappearing messages: enabled (you allow: yes (1 sec), contact allows: yes)"
      alice <## "bob updated preferences for you:"
      alice <## "Disappearing messages: enabled (you allow: yes (1 sec), contact allows: yes (1 sec))"
      alice <##> bob
      threadDelay 500000
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")])
      threadDelay 1000000
      alice <## "timed message deleted: hi"
      bob <## "timed message deleted: hi"
      alice <## "timed message deleted: hey"
      bob <## "timed message deleted: hey"
      alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
      bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])

testUpdateMultipleUserPrefs :: HasCallStack => FilePath -> IO ()
testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> do
    connectUsers alice bob
    alice #> "@bob hi bob"
    bob <# "alice> hi bob"

    connectUsers alice cath
    alice #> "@cath hi cath"
    cath <# "alice> hi cath"

    alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}, \"reactions\": {\"allow\": \"no\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
    alice <## "updated preferences:"
    alice <## "Full deletion allowed: always"
    alice <## "Message reactions allowed: no"

    bob <## "alice updated preferences for you:"
    bob <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
    bob <## "Message reactions: off (you allow: default (yes), contact allows: no)"

    cath <## "alice updated preferences for you:"
    cath <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
    cath <## "Message reactions: off (you allow: default (yes), contact allows: no)"

    alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])
    alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])

testGroupPrefsDirectForRole :: HasCallStack => FilePath -> IO ()
testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danProfile $
  \alice bob cath dan -> do
    createGroup3 "team" alice bob cath
    threadDelay 1000000
    alice ##> "/set direct #team on owner"
    alice <## "updated group preferences:"
    alice <## "Direct messages: on for owners"
    directForOwners bob
    directForOwners cath
    threadDelay 1000000
    bob ##> "@cath hello again"
    bob <## "bad chat command: direct messages not allowed"
    (cath </)

    connectUsers cath dan
    addMember "team" cath dan GRMember
    dan ##> "/j #team"
    concurrentlyN_
      [ cath <## "#team: dan joined the group",
        do
          dan <## "#team: you joined the group"
          dan
            <### [ "#team: member alice (Alice) is connected",
                   "#team: member bob (Bob) is connected"
                 ],
        do
          alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
          alice <## "#team: new member dan is connected",
        do
          bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
          bob <## "#team: new member dan is connected"
      ]

    -- dan cannot send direct messages to alice
    dan ##> "@alice hello alice"
    dan <## "bad chat command: direct messages not allowed"
    (alice </)

    -- alice (owner) can send direct messages to dan
    alice `send` "@dan hello dan"
    alice
      <### [ "member #team dan does not have direct connection, creating",
             "contact for member #team dan is created",
             "sent invitation to connect directly to member #team dan",
             WithTime "@dan hello dan"
           ]
    dan
      <### [ "#team alice is creating direct contact alice with you",
             WithTime "alice> hello dan"
           ]
    dan <## "alice (Alice): you can send messages to contact"
    concurrently_
      (alice <## "dan (Daniel): contact is connected")
      (dan <## "alice (Alice): contact is connected")

    -- now dan can send messages to alice
    dan #> "@alice hi alice"
    alice <# "dan> hi alice"
  where
    directForOwners :: HasCallStack => TestCC -> IO ()
    directForOwners cc = do
      cc <## "alice updated group #team:"
      cc <## "updated group preferences:"
      cc <## "Direct messages: on for owners"

testGroupPrefsFilesForRole :: HasCallStack => FilePath -> IO ()
testGroupPrefsFilesForRole = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> withXFTPServer $ do
    alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok")
    bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok")
    createDirectoryIfMissing True "./tests/tmp/alice"
    createDirectoryIfMissing True "./tests/tmp/bob"
    copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test1.txt"
    copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test2.txt"
    createGroup3 "team" alice bob cath
    threadDelay 1000000
    alice ##> "/set files #team on owner"
    alice <## "updated group preferences:"
    alice <## "Files and media: on for owners"
    filesForOwners bob
    filesForOwners cath
    threadDelay 1000000
    bob ##> "/f #team test2.txt"
    bob <## "bad chat command: feature not allowed Files and media"
    (alice </)
    (cath </)
    alice #> "/f #team test1.txt"
    alice <## "use /fc 1 to cancel sending"
    alice <## "completed uploading file 1 (test1.txt) for #team"
    bob <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)"
    bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
    cath <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)"
    cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
  where
    filesForOwners :: HasCallStack => TestCC -> IO ()
    filesForOwners cc = do
      cc <## "alice updated group #team:"
      cc <## "updated group preferences:"
      cc <## "Files and media: on for owners"

testGroupPrefsSimplexLinksForRole :: HasCallStack => FilePath -> IO ()
testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfile $
  \alice bob cath -> withXFTPServer $ do
    createGroup3 "team" alice bob cath
    threadDelay 1000000
    alice ##> "/set links #team on owner"
    alice <## "updated group preferences:"
    alice <## "SimpleX links: on for owners"
    linksForOwners bob
    linksForOwners cath
    threadDelay 1000000
    bob ##> "/c"
    inv <- getInvitation bob
    bob ##> ("#team \"" <> inv <> "\\ntest\"")
    bob <## "bad chat command: feature not allowed SimpleX links"
    bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}")
    bob <## "bad chat command: feature not allowed SimpleX links"
    (alice </)
    (cath </)
    bob `send` ("@alice \"" <> inv <> "\\ntest\"")
    bob <# ("@alice " <> inv)
    bob <## "test"
    alice <# ("bob> " <> inv)
    alice <## "test"
    bob ##> "#team <- @alice https://simplex.chat"
    bob <## "bad chat command: feature not allowed SimpleX links"
    alice #> ("#team " <> inv)
    bob <# ("#team alice> " <> inv)
    cath <# ("#team alice> " <> inv)
  where
    linksForOwners :: HasCallStack => TestCC -> IO ()
    linksForOwners cc = do
      cc <## "alice updated group #team:"
      cc <## "updated group preferences:"
      cc <## "SimpleX links: on for owners"

testSetUITheme :: HasCallStack => FilePath -> IO ()
testSetUITheme =
  testChat2 aliceProfile bobProfile $ \alice bob -> do
    connectUsers alice bob
    alice ##> "/g team"
    alice <## "group #team is created"
    alice <## "to add members use /a team <name> or /create link #team"
    alice #$> ("/_set theme user 1 " <> theme UCMDark, id, "ok")
    alice #$> ("/_set theme @2 " <> theme UCMDark, id, "ok")
    alice #$> ("/_set theme #1 " <> theme UCMDark, id, "ok")
    alice ##> "/u"
    userInfo alice "alice (Alice)"
    alice <## ("UI themes: " <> theme UCMDark)
    alice ##> "/create user alice2"
    userInfo alice "alice2"
    alice ##> "/u alice"
    userInfo alice "alice (Alice)"
    alice <## ("UI themes: " <> theme UCMDark)
    alice ##> "/i @bob"
    contactInfo alice
    alice <## ("UI themes: " <> theme UCMDark)
    alice ##> "/i #team"
    groupInfo alice
    alice <## ("UI themes: " <> theme UCMDark)
    alice #$> ("/_set theme user 1", id, "ok")
    alice #$> ("/_set theme @2", id, "ok")
    alice #$> ("/_set theme #1", id, "ok")
    alice ##> "/u"
    userInfo alice "alice (Alice)"
    alice ##> "/i @bob"
    contactInfo alice
    alice ##> "/i #team"
    groupInfo alice
  where
    theme cm = T.unpack $ encodeJSON UIThemeEntityOverrides {light = Nothing, dark = Just $ UIThemeEntityOverride cm Nothing defaultUIColors}
    userInfo a name = do
      a <## ("user profile: " <> name)
      a <## "use /p <display name> to change it"
      a <## "(the updated profile will be sent to all your contacts)"
    contactInfo a = do
      a <## "contact ID: 2"
      a <## "receiving messages via: localhost"
      a <## "sending messages via: localhost"
      a <## "you've shared main profile with this contact"
      a <## "connection not verified, use /code command to see security code"
      a <## "quantum resistant end-to-end encryption"
      a <## "peer chat protocol version range: (Version 1, Version 9)"
    groupInfo a = do
      a <## "group ID: 1"
      a <## "current members: 1"
