packages feed

hedis-0.16.3: test/Tests.hs

{-# LANGUAGE CPP, OverloadedStrings, RecordWildCards, LambdaCase, OverloadedLists, TypeApplications #-}
module Tests where


#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
import Data.Monoid (mappend)
#endif
import qualified Control.Concurrent.Async as Async
import Control.Exception (try)
import Control.Concurrent
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Except
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as Char8
import Data.Either (isRight)
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import Data.Time
import Data.Time.Clock.POSIX
import qualified Test.Framework as Test (Test)
import qualified Test.Framework.Providers.HUnit as Test (testCase)
import qualified Test.HUnit as HUnit
import qualified Test.HUnit.Lang as HUnit.Lang
import Network.Socket (PortNumber)

import Database.Redis
import Data.Either (fromRight)

------------------------------------------------------------------------------
-- helpers
--
type Test = Connection -> Test.Test

testCase :: String -> Redis () -> Test
testCase name r conn = Test.testCase name $ do
    withTimeLimit 0.5 $ runRedis conn $ flushdb >>=? Ok >> r
  where
    withTimeLimit limit act = do
        start <- getCurrentTime
        _ <- act
        deltaT <-fmap (`diffUTCTime` start) getCurrentTime
        when (deltaT > limit) $
            putStrLn $ name ++ ": " ++ show deltaT

(>>=?) :: (Eq a, Show a) => Redis (Either Reply a) -> a -> Redis ()
redis >>=? expected = redis >>@? (expected HUnit.@=?)

(>>@?) :: (Eq a, Show a) => Redis (Either Reply a) -> (a -> HUnit.Assertion) -> Redis ()
redis >>@? predicate = do
    a <- redis
    liftIO $ case a of
        Left reply -> HUnit.assertFailure $ "Redis error: " ++ show reply
        Right actual -> predicate actual

(<|?>) :: HUnit.Assertion -> HUnit.Assertion -> HUnit.Assertion
a <|?> b = do
    resultA <- HUnit.Lang.performTestCase a
    case resultA of
        HUnit.Lang.Success        -> a
        HUnit.Lang.Failure _ errA -> tryB errA
        HUnit.Lang.Error   _ errA -> tryB errA
        where tryB errA = do
                        resultB <- HUnit.Lang.performTestCase b
                        case resultB of
                            HUnit.Lang.Success        -> b
                            HUnit.Lang.Failure _ errB -> concatErrors errA errB
                            HUnit.Lang.Error   _ errB -> concatErrors errA errB
              concatErrors errA errB = HUnit.Lang.assertFailure ("{" ++ errA ++ "\nOR\n" ++ errB ++ "\n}: Failed")


assert :: Bool -> Redis ()
assert = liftIO . HUnit.assert

isUnknownCommandReply :: Reply -> Bool
isUnknownCommandReply (Error message) = "unknown command" `BS.isInfixOf` message
isUnknownCommandReply _ = False

isHotkeysInactiveReply :: Reply -> Bool
isHotkeysInactiveReply (Error message) =
    "not currently active" `BS.isInfixOf` message || "tracking is not active" `BS.isInfixOf` message
isHotkeysInactiveReply (Bulk Nothing) = True
isHotkeysInactiveReply _ = False

------------------------------------------------------------------------------
-- Miscellaneous
--
testsMisc :: [Test]
testsMisc =
    [ testConstantSpacePipelining, testForceErrorReply, testPipelining
    , testEvalReplies, testGeo, testWaitCommands
    ]

testConstantSpacePipelining :: Test
testConstantSpacePipelining = testCase "constant-space pipelining" $ do
    -- This testcase should not exceed the maximum heap size, as set in
    -- the run-test.sh script.
    replicateM_ 100000 ping
    -- If the program didn't crash, pipelining takes constant memory.
    assert True

testForceErrorReply :: Test
testForceErrorReply = testCase "force error reply" $ do
    set "key" "value" >>= \case
      Left _ -> error "impossible"
      _ -> return ()
    -- key is not a hash -> wrong kind of value
    reply <- hkeys "key"
    assert $ case reply of
        Left (Error _) -> True
        _              -> False

testPipelining :: Test
testPipelining = testCase "pipelining" $ do
    let n = 100
    tPipe <- deltaT $ do
        pongs <- replicateM n ping
        assert $ pongs == replicate n (Right Pong)

    tNoPipe <- deltaT $ replicateM_ n (ping >>=? Pong)
    -- pipelining should at least be twice as fast.
    assert $ tNoPipe / tPipe > 2
  where
    deltaT redis = do
        start <- liftIO $ getCurrentTime
        _ <- redis
        liftIO $ fmap (`diffUTCTime` start) getCurrentTime

testEvalReplies :: Test
testEvalReplies conn = testCase "eval unused replies" go conn
  where
    go = do
      _ <- liftIO $ runRedis conn $ set "key-12" "value"
      result <- liftIO $ do
         threadDelay $ 10 ^ (5 :: Int)
         mvar <- newEmptyMVar
         _ <-
           (Async.wait =<< Async.async (runRedis conn (get "key-12"))) >>= putMVar mvar
         takeMVar mvar
      pure result >>=? Just "value"

testGeo :: Test
testGeo = testCase "geo" $ do
    geoadd "{geo}cities" [(13.361389, 38.115556, "Palermo"), (15.087269, 37.502669, "Catania")] >>=? 2
    geoaddOpts "{geo}cities"
        [(13.361389, 38.115556, "Palermo"), (12.496366, 41.902782, "Rome")]
        defaultGeoAddOpts { geoAddCondition = Just Nx, geoAddChange = True } >>=? 1
    geodist "{geo}cities" "Palermo" "Rome" (Just GeoKilometers) >>@? \actual ->
        case actual of
            Just dist -> HUnit.assertBool "Rome should have been inserted by GEOADD NX" (dist > 400)
            Nothing -> HUnit.assertFailure "GEODIST Palermo Rome returned Nothing"
    geoaddOpts "{geo}cities"
        [(9.1900, 45.4642, "Milan")]
        defaultGeoAddOpts { geoAddCondition = Just Xx } >>=? 0

    geodist "{geo}cities" "Palermo" "Catania" Nothing >>@? \actual ->
        case actual of
            Just dist -> HUnit.assertBool "unexpected GEODIST distance in meters" (abs (dist - 166274.1516) < 1000)
            Nothing -> HUnit.assertFailure "GEODIST returned Nothing"

    geopos "{geo}cities" ["Palermo", "Catania"] >>@? \positions ->
        case positions of
            [Just palermo, Just catania] -> do
                assertApprox "Palermo longitude" 13.361389 (geoLongitude palermo)
                assertApprox "Palermo latitude" 38.115556 (geoLatitude palermo)
                assertApprox "Catania longitude" 15.087269 (geoLongitude catania)
                assertApprox "Catania latitude" 37.502669 (geoLatitude catania)
            _ -> HUnit.assertFailure $ "Unexpected GEOPOS response: " ++ show positions

    let searchOpts = defaultGeoSearchOpts
            { geoSearchWithDist = True
            , geoSearchOrder = Just GeoAsc
            }

    geoSearch "{geo}cities" (GeoSearchFromMember "Palermo") (GeoSearchByRadius 200 GeoKilometers) searchOpts >>@? \locations ->
        case locations of
            [palermo, catania] -> do
                HUnit.assertEqual "GEOSEARCH center member" "Palermo" (geoLocationMember palermo)
                HUnit.assertBool "GEOSEARCH distance should be zero for center member" (maybe False (< 0.001) (geoLocationDist palermo))
                HUnit.assertEqual "GEOSEARCH second member" "Catania" (geoLocationMember catania)
            _ -> HUnit.assertFailure $ "Unexpected GEOSEARCH response: " ++ show locations

    geoSearchStore "{geo}near" "{geo}cities" (GeoSearchFromLonLat 15 37) (GeoSearchByRadius 200 GeoKilometers)
        (defaultGeoSearchStoreOpts { geoSearchStoreStoredist = True }) >>=? 2

    zrangeWithscores "{geo}near" 0 (-1) >>@? \members ->
        case members of
            [(firstCity, firstDistance), (secondCity, secondDistance)] -> do
                HUnit.assertEqual "closest stored city" "Catania" firstCity
                HUnit.assertEqual "second stored city" "Palermo" secondCity
                HUnit.assertBool "stored distances should be increasing" (firstDistance < secondDistance)
            _ -> HUnit.assertFailure $ "Unexpected GEOSEARCHSTORE response: " ++ show members
  where
    assertApprox label expected actual =
        HUnit.assertBool label (abs (expected - actual) < 0.0001)

testWaitCommands :: Test
testWaitCommands = testCase "wait commands" $ do
    set "wait:key" "value" >>=? Ok
    wait 0 0 >>=? 0

    waitaofResult <- waitaof 0 0 0
    liftIO $ case waitaofResult of
        Right result -> WaitAofResult 0 0 HUnit.@=? result
        Left reply | isUnknownCommandReply reply -> pure ()
        Left reply -> HUnit.assertFailure $ "Unexpected WAITAOF reply: " ++ show reply

------------------------------------------------------------------------------
-- Keys
--
testKeys :: Test
testKeys = testCase "keys" $ do
    set "{same}key" "value"     >>=? Ok
    get "{same}key"             >>=? Just "value"
    exists "{same}key"          >>=? True
    expire "{same}key" 1        >>=? True
    pexpire "{same}key" 1000    >>=? True
    ttl "{same}key" >>= \case
      Left _ -> error "error"
      Right t -> do
        assert $ elem @[] t [0..1]
        pttl "{same}key" >>= \case
          Left _ -> error "error"
          Right pt -> do
            assert $ elem @[] pt [990..1000]
            persist "{same}key"         >>=? True
            dump "{same}key" >>= \case
              Left _ -> error "impossible"
              Right s -> do
                restore "{same}key'" 0 s          >>=? Ok
                rename "{same}key" "{same}key'"   >>=? Ok
                renamenx "{same}key'" "{same}key" >>=? True
                del (NE.fromList ["{same}key"])   >>=? 1

testCopy :: Test
testCopy = testCase "copy" $ do
    set "dolly" "sheep" >>=? Ok
    copy "dolly" "clone" >>=? True
    get "clone" >>=? Just "sheep"
    copy "dolly" "clone" >>=? False
    copyOpts "dolly" "clone" defaultCopyOpts { copyReplace = True } >>=? True

testKeysNoncluster :: Test
testKeysNoncluster = testCase "keysNoncluster" $ do
    set "key" "value"     >>=? Ok
    keys "*"              >>=? ["key"]
    randomkey             >>=? Just "key"
    move "key" 13         >>=? True
    select 13             >>=? Ok
    get "key"             >>=? Just "value"
    select 0              >>=? Ok

testExpireAt :: Test
testExpireAt = testCase "expireat" $ do
    set "key" "value"             >>=? Ok
    t <- ceiling . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime
    let expiry = t+1
    expireat "key" expiry         >>=? True
    pexpireat "key" (expiry*1000) >>=? True

testSort :: Test
testSort = testCase "sort" $ do
    lpush "ids"     ["1","2","3"]                >>=? 3
    sort "ids" defaultSortOpts                   >>=? ["1","2","3"]
    sortStore "ids" "anotherKey" defaultSortOpts >>=? 3
    mset
         [("weight_1","1")
         ,("weight_2","2")
         ,("weight_3","3")
         ,("object_1","foo")
         ,("object_2","bar")
         ,("object_3","baz")
         ] >>= \case
      Left _ -> error "error"
      _ -> return ()
    let opts = defaultSortOpts { sortOrder = Desc, sortAlpha = True
                               , sortLimit = (1,2)
                               , sortBy    = Just "weight_*"
                               , sortGet   = ["#", "object_*"] }
    sort "ids" opts >>=? ["2", "bar", "1", "foo"]


testGetType :: Test
testGetType = testCase "getType" $ do
    getType "key"     >>=? None
    forM_ @[] ts $ \(setKey, typ) -> do
        setKey
        getType "key" >>=? typ
        del (NE.fromList ["key"])   >>=? 1
  where
    ts = [ (set "key" "value"                         >>=? Ok,   String)
         , (hset "key" [("field"::ByteString, "value"::ByteString)] >>=? 1,    Hash)
         , (lpush "key" ["value"]                     >>=? 1,    List)
         , (sadd "key" ["member"]                     >>=? 1,    Set)
         , (zadd "key" [(42,"member"),(12.3,"value")] >>=? 2,    ZSet)
         ]

testObject :: Test
testObject = testCase "object" $ do
    set "key" "value"    >>=? Ok
    objectRefcount "key" >>=? 1
    objectEncoding "key" >>= \case
       Left _ -> error "error"
       _ -> return ()
    objectIdletime "key" >>=? 0
    return ()

------------------------------------------------------------------------------
-- Strings
--
testsStrings :: [Test]
testsStrings = [testStrings, testStringCommands6, testBitops]

testStrings :: Test
testStrings = testCase "strings" $ do
    setnx "key" "value"                           >>=? True
    getset "key" "hello"                          >>=? Just "value"
    append "key" "world"                          >>=? 10
    strlen "key"                                  >>=? 10
    setrange "key" 0 "hello"                      >>=? 10
    getrange "key" 0 4                            >>=? "hello"
    substr "key" 0 4                              >>=? "hello"
    mset [("{same}k1","v1"), ("{same}k2","v2")]   >>=? Ok
    msetnx [("{same}k1","v1"), ("{same}k2","v2")] >>=? False
    mget ["key"]                                  >>=? [Just "helloworld"]
    setex "key" 1 "42"                            >>=? Ok
    psetex "key" 1000 "42"                        >>=? Ok
    decr "key"                                    >>=? 41
    decrby "key" 1                                >>=? 40
    incr "key"                                    >>=? 41
    incrby "key" 1                                >>=? 42
    incrbyfloat "key" 1                           >>=? 43
    del (NE.fromList ["key"])                     >>=? 1
    setbit "key" 42 "1"                           >>=? 0
    getbit "key" 42                               >>=? 1
    bitcount "key"                                >>=? 1
    bitcountRange "key" 0 (-1)                    >>=? 1

testStringCommands6 :: Test
testStringCommands6 = testCase "strings redis 6" $ do
    set "mykey" "Hello" >>=? Ok
    getdel "mykey" >>=? Just "Hello"
    get "mykey" >>=? Nothing
    set "mykey" "Hello" >>=? Ok
    getexOpts "mykey" defaultGetExOpts { getExSeconds = Just 10 } >>=? Just "Hello"
    ttl "mykey" >>@? \value ->
        HUnit.assertBool "GETEX should set ttl" (value >= 0 && value <= 10)

testStringCommands84 :: Test
testStringCommands84 = testCase "strings redis 8.4" $ do
    set "digest-key" "Hello world" >>=? Ok
    digest "digest-key" >>=? Just "b6acb9d84a38ff74"
    delexWhen "digest-key" (DelexIfEq "Goodbye") >>=? False
    get "digest-key" >>=? Just "Hello world"
    digest "digest-key" >>= \case
        Left reply -> liftIO $ HUnit.assertFailure $ "Redis error: " ++ show reply
        Right (Just digestValue) -> do
            delexWhen "digest-key" (DelexIfDigestEq digestValue) >>=? True
            get "digest-key" >>=? Nothing
        Right Nothing -> liftIO $ HUnit.assertFailure "DIGEST should return a digest for an existing string key"

    msetexOpts (("k1", "v1") NE.:| [("k2", "v2")]) defaultSetOpts { setSeconds = Just 60 } >>=? True
    mget ["k1", "k2"] >>=? [Just "v1", Just "v2"]
    ttl "k1" >>@? \value ->
        HUnit.assertBool "MSETEX should set ttl" (value >= 0 && value <= 60)

    msetexOpts (("k1", "new-v1") NE.:| [("k3", "v3")]) defaultSetOpts { setCondition = Just Nx } >>=? False
    get "k1" >>=? Just "v1"
    get "k3" >>=? Nothing

    msetexOpts (("k1", "new-v1") NE.:| [("k2", "new-v2")]) defaultSetOpts { setCondition = Just Xx } >>=? True
    mget ["k1", "k2"] >>=? [Just "new-v1", Just "new-v2"]

testBitops :: Test
testBitops = testCase "bitops" $ do
    set "{same}k1" "a"                           >>=? Ok
    set "{same}k2" "b"                           >>=? Ok
    bitopAnd "{same}k3" ["{same}k1", "{same}k2"] >>=? 1
    bitopOr "{same}k3" ["{same}k1", "{same}k2"]  >>=? 1
    bitopXor "{same}k3" ["{same}k1", "{same}k2"] >>=? 1
    bitopNot "{same}k3" "{same}k1"               >>=? 1

------------------------------------------------------------------------------
-- Hashes
--
testHashes :: Test
testHashes = testCase "hashes" $ do
    hset "key" [("field"::ByteString, "another"::ByteString)] >>=? 1
    hset "key" [("field"::ByteString, "another"::ByteString)] >>=? 0
    hset "key" [("field"::ByteString, "value"::ByteString)]   >>=? 0
    hsetnx "key" "field" "value" >>=? False
    hexists "key" "field"        >>=? True
    hlen "key"                   >>=? 1
    hget "key" "field"           >>=? Just "value"
    hmget "key" ["field", "-"]   >>=? [Just "value", Nothing]
    hgetall "key"                >>=? [("field","value")]
    hkeys "key"                  >>=? ["field"]
    hvals "key"                  >>=? ["value"]
    hdel "key" ["field"]         >>=? 1
    hmset "key" [("field","40")] >>=? Ok
    hincrby "key" "field" 2      >>=? 42
    hincrbyfloat "key" "field" 2 >>=? 44
    hset "coin" [("heads","obverse"),("tails","reverse"),("edge","null")] >>=? 3
    hrandfield "coin" >>@? \field ->
        HUnit.assertBool "HRANDFIELD should return an existing field" (field `elem` ([Just "heads", Just "tails", Just "edge"] :: [Maybe ByteString]))
    hrandfieldCount "coin" 2 >>@? \fields -> do
        HUnit.assertEqual "HRANDFIELD count" 2 (length fields)
        HUnit.assertBool "HRANDFIELD count should return distinct fields" (fields == L.nub fields)
        HUnit.assertBool "HRANDFIELD count should return existing fields" (all (`elem` (["heads", "tails", "edge"] :: [ByteString])) fields)
    hrandfieldCountWithValues "coin" 2 >>@? \fields -> do
        HUnit.assertEqual "HRANDFIELD WITHVALUES count" 2 (length fields)
        HUnit.assertBool "HRANDFIELD WITHVALUES should return existing field/value pairs" $
            all (`elem` ([("heads", "obverse"), ("tails", "reverse"), ("edge", "null")] :: [(ByteString, ByteString)])) fields

testHashes8 :: Test
testHashes8 = testCase "hashes redis 8" $ do
    hsetexOpts "myhash" (("field1", "Hello") NE.:| [("field2", "World")])
        defaultHSetExOpts { hSetExSeconds = Just 60 }
        >>=? True
    httl "myhash" ["field1"] >>@? \case
        (HashFieldExpirationInfo value:_) ->
           HUnit.assertBool ("HSETEX should set ttl: " <> show value) (value >= 0 && value <= 60)
        x -> HUnit.assertFailure $ "HTTL should return field expiration info for field1" <> show x

    hgetexOpts "myhash" ("field1" NE.:| ["field2", "missing"])
        defaultHGetExOpts { hGetExPersist = True }
        >>=? [Just "Hello", Just "World", Nothing]
    ttl "myhash" >>=? (-1)

    hgetdel "myhash" ("field1" NE.:| ["field2", "missing"])
        >>=? [Just "Hello", Just "World", Nothing]
    hgetall "myhash" >>=? []

    hsetexOpts "myhash" (("field1", "Hello") NE.:| [])
        defaultHSetExOpts { hSetExCondition = Just HSetExFnx }
        >>=? True
    hsetexOpts "myhash" (("field1", "World") NE.:| [])
        defaultHSetExOpts { hSetExCondition = Just HSetExFnx }
        >>=? False
    hsetexOpts "myhash" (("field1", "World") NE.:| [])
        defaultHSetExOpts { hSetExCondition = Just HSetExFxx }
        >>=? True
    hget "myhash" "field1" >>=? Just "World"

------------------------------------------------------------------------------
-- Lists
--
testsLists :: [Test]
testsLists =
    [testLists, testListCommands6, testBpop]

testLists :: Test
testLists = testCase "lists" $ do
    lpushx "notAKey" ["-" :: ByteString] >>=? 0
    rpushx "notAKey" ["-" :: ByteString] >>=? 0
    lpush "key" ["value"]         >>=? 1
    lpop "key"                    >>=? Just "value"
    rpush "key" ["value"]         >>=? 1
    rpop "key"                    >>=? Just "value"
    rpush "key" ["v2"]            >>=? 1
    linsertBefore "key" "v2" "v1" >>=? 2
    linsertAfter "key" "v2" "v3"  >>=? 3
    lindex "key" 0                >>=? Just "v1"
    lrange "key" 0 (-1)           >>=? ["v1", "v2", "v3"]
    lset "key" 1 "v2"             >>=? Ok
    lrem "key" 0 "v2"             >>=? 1
    llen "key"                    >>=? 2
    ltrim "key" 0 1               >>=? Ok
    del ("key" NE.:| [])
    -- keys are pushed sequentially so this will result in a list with value1, value2, value3
    lpush "key" ["value3", "value2", "value1"] >>=? 3
    lpopCount "key" 2 >>=? ["value1", "value2"]
    lpush "key" ["value2", "value1"] >>=? 3
    rpopCount "key" 2 >>=? ["value3", "value2"]
    del ("key" NE.:| [])
    lpush "key" ["value3", "value2", "value1"] >>=? 3
    lpopCount "key" 4 >>=? ["value1", "value2", "value3"]
    del ("key" NE.:| [])
    return ()

testListCommands6 :: Test
testListCommands6 = testCase "lists redis 6" $ do
    rpush "mylist" ["a", "b", "c", "d", "1", "2", "3", "4", "3", "3", "3"] >>=? 11
    lpos "mylist" "3" >>=? Just 6
    lposCount "mylist" "3" 0 >>=? [6, 8, 9, 10]
    lposOpts "mylist" "3" defaultLPosOpts { lposRank = Just 2 } >>=? Just 8

    rpush "{same}src" ["one", "two", "three"] >>=? 3
    lmove "{same}src" "{same}dst" ListLeft ListRight >>=? Just "one"
    lrange "{same}src" 0 (-1) >>=? ["two", "three"]
    lrange "{same}dst" 0 (-1) >>=? ["one"]

    rpush "{same}src2" ["one", "two", "three"] >>=? 3
    blmove "{same}src2" "{same}dst2" ListRight ListLeft 1 >>=? Just "three"
    lrange "{same}src2" 0 (-1) >>=? ["one", "two"]
    lrange "{same}dst2" 0 (-1) >>=? ["three"]

testBpop :: Test
testBpop = testCase "blocking push/pop" $ do
    lpush "{same}key" ["v3","v2","v1"] >>=? 3
    blpop ["{same}key"] 1              >>=? Just ("{same}key","v1")
    brpop ["{same}key"] 1              >>=? Just ("{same}key","v3")
    rpush "{same}k1" ["v1","v2"]       >>=? 2
    brpoplpush "{same}k1" "{same}k2" 1 >>=? Just "v2"
    rpoplpush "{same}k1" "{same}k2"    >>=? Just "v1"

------------------------------------------------------------------------------
-- Sets
--
testsSets :: [Test]
testsSets = [testSets, testSMIsMember, testSetAlgebra]

testSets :: Test
testSets = testCase "sets" $ do
    sadd "set" (NE.fromList  ["member"]) >>=? 1
    sismember "set" "member"    >>=? True
    scard "set"                 >>=? 1
    smembers "set"              >>=? ["member"]
    srandmember "set"           >>=? Just "member"
    spop "set"                  >>=? Just "member"
    srem "set" (NE.fromList ["member"]) >>=? 0
    smove "{same}set" "{same}set'" "member" >>=? False
    _ <- sadd "set" (NE.fromList ["member1", "member2"])
    (fmap L.sort <$> spopN "set" 2) >>=? ["member1", "member2"]
    _ <- sadd "set" (NE.fromList ["member1", "member2"])
    (fmap L.sort <$> srandmemberN "set" 2) >>=? ["member1", "member2"]

testSMIsMember :: Test
testSMIsMember = testCase "smismember" $ do
    sadd "myset" (NE.fromList ["one"]) >>=? 1
    smismember "myset" ("one" NE.:| ["notamember"]) >>=? [True, False]

testSetAlgebra :: Test
testSetAlgebra = testCase "set algebra" $ do
    sadd "{same}s1" (NE.fromList ["member"])        >>=? 1
    sdiff ["{same}s1", "{same}s2"]                  >>=? ["member"]
    sunion ["{same}s1", "{same}s2"]                 >>=? ["member"]
    sinter ["{same}s1", "{same}s2"]                 >>=? []
    sdiffstore "{same}s3" ["{same}s1", "{same}s2"]  >>=? 1
    sunionstore "{same}s3" ["{same}s1", "{same}s2"] >>=? 1
    sinterstore "{same}s3" ["{same}s1", "{same}s2"] >>=? 0

------------------------------------------------------------------------------
-- Sorted Sets
--
testsZSets :: [Test]
testsZSets = [testZSets, testSortedSetCommands6, testZStore]

testZSets :: Test
testZSets = testCase "sorted sets" $ do
    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")]          >>=? 3
    zcard "key"                                       >>=? 3
    zscore "key" "v3"                                 >>=? Just 40
    zincrby "key" 2 "v3"                              >>=? 42

    zrank "key" "v1"                                  >>=? Just 0
    zrevrank "key" "v1"                               >>=? Just 2
    zcount "key" 10 100                               >>=? 1

    zrange "key" 0 1                                  >>=? ["v1","v2"]
    zrevrange "key" 0 1                               >>=? ["v3","v2"]
    zrangeWithscores "key" 0 1                        >>=? [("v1",1),("v2",2)]
    zrevrangeWithscores "key" 0 1                     >>=? [("v3",42),("v2",2)]
    zrangebyscore "key" 0.5 1.5                       >>=? ["v1"]
    zrangebyscoreWithscores "key" 0.5 1.5             >>=? [("v1",1)]
    zrangebyscoreWithscores "key" (-inf) inf          >>=? [("v1",1.0),("v2",2.0),("v3",42.0)]
    zrangebyscoreLimit "key" 0.5 2.5 0 1              >>=? ["v1"]
    zrangebyscoreWithscoresLimit "key" 0.5 2.5 0 1    >>=? [("v1",1)]
    zrevrangebyscore "key" 1.5 0.5                    >>=? ["v1"]
    zrevrangebyscoreWithscores "key" 1.5 0.5          >>=? [("v1",1)]
    zrevrangebyscoreLimit "key" 2.5 0.5 0 1           >>=? ["v2"]
    zrevrangebyscoreWithscoresLimit "key" 2.5 0.5 0 1 >>=? [("v2",2)]

    zrem "key" (NE.fromList ["v2"])                   >>=? 1
    zremrangebyscore "key" 10 100                     >>=? 1
    zremrangebyrank "key" 0 0                         >>=? 1

testSortedSetCommands6 :: Test
testSortedSetCommands6 = testCase "sorted sets redis 6" $ do
    zadd "{same}zset1" [(1, "one"), (2, "two"), (3, "three")] >>=? 3
    zadd "{same}zset2" [(1, "one"), (2, "two")] >>=? 2

    zdiff ("{same}zset1" NE.:| ["{same}zset2"]) >>=? ["three"]
    zdiffWithscores ("{same}zset1" NE.:| ["{same}zset2"]) >>=? [("three", 3)]
    zdiffstore "{same}out" ("{same}zset1" NE.:| ["{same}zset2"]) >>=? 1
    zrangeWithscores "{same}out" 0 (-1) >>=? [("three", 3)]

    zinter ("{same}zset1" NE.:| ["{same}zset2"]) >>=? ["one", "two"]
    zinterWithscores ("{same}zset1" NE.:| ["{same}zset2"]) >>=? [("one", 2), ("two", 4)]
    zinterWithscoresOpts ("{same}zset1" NE.:| ["{same}zset2"])
        defaultZAggregateOpts { zAggregateWeights = [2, 3], zAggregateAggregate = Max }
        >>=? [("one", 3), ("two", 6)]

    zunion ("{same}zset1" NE.:| ["{same}zset2"]) >>=? ["one", "three", "two"]
    zunionWithscores ("{same}zset1" NE.:| ["{same}zset2"]) >>=? [("one", 2), ("three", 3), ("two", 4)]
    zmscore "{same}zset1" ("one" NE.:| ["notamember"]) >>=? [Just 1, Nothing]

    zrandmember "{same}zset1" >>@? \member ->
        HUnit.assertBool "ZRANDMEMBER should return an existing member" (member `elem` ([Just "one", Just "two", Just "three"] :: [Maybe ByteString]))
    zrandmemberN "{same}zset1" 2 >>@? \members -> do
        HUnit.assertEqual "ZRANDMEMBER count" 2 (length members)
        HUnit.assertBool "ZRANDMEMBER count should return existing members" (all (`elem` (["one", "two", "three"] :: [ByteString])) members)
    zrandmemberWithscores "{same}zset1" 2 >>@? \members -> do
        HUnit.assertEqual "ZRANDMEMBER WITHSCORES count" 2 (length members)
        HUnit.assertBool "ZRANDMEMBER WITHSCORES should return valid pairs" $
            all (`elem` ([("one", 1), ("two", 2), ("three", 3)] :: [(ByteString, Double)])) members

    zrangestore "{same}newzset" "{same}zset1" 2 (-1) >>=? 1
    zrange "{same}newzset" 0 (-1) >>=? ["three"]

--  testZSets7 :: Test
--  testZSets7 = testCase "sorted sets: redis 7" $ do
--      zadd "key" [(2,"v1"),(0,"v2"),(40,"v3")]          >>=? 3
--      zrankWithScore "key" "v1"                         >>=? Just  (1, 2)

testZStore :: Test
testZStore = testCase "zunionstore/zinterstore" $ do
    zadd "{same}k1" [(1, "v1"), (2, "v2")] >>= \case
      Left _ -> error "error"
      _ -> return ()
    zadd "{same}k2" [(2, "v2"), (3, "v3")] >>= \case
      Left _ -> error "error"
      _ -> return ()
    zinterstore "{same}newkey" ["{same}k1","{same}k2"] Sum                >>=? 1
    zinterstoreWeights "{same}newkey" [("{same}k1",1),("{same}k2",2)] Max >>=? 1
    zunionstore "{same}newkey" ["{same}k1","{same}k2"] Sum                >>=? 3
    zunionstoreWeights "{same}newkey" [("{same}k1",1),("{same}k2",2)] Min >>=? 3

------------------------------------------------------------------------------
-- HyperLogLog
--

testHyperLogLog :: Test
testHyperLogLog = testCase "hyperloglog" $ do
  -- test creation
  pfadd "hll1" ["a"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfcount ["hll1"] >>=? 1
  -- test cardinality
  pfadd "hll1" ["a"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfcount ["hll1"] >>=? 1
  pfadd "hll1" ["b", "c", "foo", "bar"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfcount ["hll1"] >>=? 5
  -- test merge
  pfadd "{same}hll2" ["1", "2", "3"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfadd "{same}hll3" ["4", "5", "6"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfmerge "{same}hll4" ["{same}hll2", "{same}hll3"] >>= \case
      Left _ -> error "error"
      _ -> return ()
  pfcount ["{same}hll4"] >>=? 6
  -- test union cardinality
  pfcount ["{same}hll2", "{same}hll3"] >>=? 6

------------------------------------------------------------------------------
-- Bloom Filters
--
testBloomFilter :: Test
testBloomFilter = testCase "bloom filter" $ do
    liftIO $ putStrLn "Testing bloom filter with default parameters..."
    bfadd "bf1" "observation1" >>=? True
    liftIO $ putStrLn "Testing bloom filter info commands..."
    bfinfoSize "bf1" >>=? [240]
    liftIO $ putStrLn "Testing bloom filter info commands with error handling..."
    Right [infoSize] <- bfinfoSize "bf1"
    liftIO $ putStrLn $ "Bloom filter info: " ++ show infoSize
    bfinfo "bf1" >>=? BFInfo
        { bfInfoCapacity = 100
        , bfInfoSize = infoSize
        , bfInfoFilters = 1
        , bfInfoItems = 1
        , bfInfoExpansion = 2
        }
    bfinfoCapacity "bf1" >>=? [100]
    bfinfoFilters "bf1" >>=? [1]
    bfinfoItems "bf1" >>=? [1]
    bfinfoExpansion "bf1" >>=? [Just 2]
    bfcard "bf1" >>=? 1
    bfcard "bf_new" >>=? 0


    liftIO $ putStrLn "Testing bloom filter with custom parameters..."

    bfreserve "bf" 0.01 1000 >>=? Ok
    bfadd "bf" "item1" >>=? True
    bfexists "bf" "item1" >>=? True
    bfexists "bf" "item2" >>=? False

    liftIO $ putStrLn "Testing bloom filter multi-item commands..."

    bfmadd "bfm" ("item1" NE.:| ["item2", "item2"]) >>=? [True, True, False]
    bfmexists "bfm" ("item1" NE.:| ["item2", "item3"]) >>=? [True, True, False]

    liftIO $ putStrLn "Testing bloom filter insert commands with custom parameters..."

    bfinsert "bfi" ("item1" NE.:| ["item2"]) >>=? [True, True]
    bfinsertOpts "bf_insert" ("item1" NE.:| ["item2"]) defaultBFInsertOpts
        { bfInsertCapacity = Just 1000
        , bfInsertError = Just 0.01
        , bfInsertExpansion = Just 3
        , bfInsertNonScaling = True
        } >>=? [True, True]


    bfinfoCapacity "bf_insert" >>=? [1000]
    bfinfoExpansion "bf_insert" >>=? [Nothing]

    bfinsertOpts "bf_insert1" ("item1" NE.:| ["item2"]) defaultBFInsertOpts
        { bfInsertCapacity = Just 1000
        , bfInsertError = Just 0.01
        , bfInsertExpansion = Just 4
        , bfInsertNonScaling = False
        } >>=? [True, True]
    bfinfoExpansion "bf_insert1" >>=? [Just 4]


    bfreserveOpts "bf_exp" 0.01 1000 defaultBFReserveOpts
        { bfReserveExpansion = Just 2
        } >>=? Ok
    bfinfoExpansion "bf_exp" >>=? [Just 2]


    bfreserve "bfdump" 0.1 10 >>=? Ok
    bfadd "bfdump" "item1" >>=? True

-- Count-Min Sketches
--
testCountMinSketch :: Test
testCountMinSketch = testCase "count-min sketch" $ do
    cmsinitbydim "cms1" 20 5 >>=? Ok
    cmsinfo "cms1" >>=? CMSInfo
        { cmsInfoWidth = 20
        , cmsInfoDepth = 5
        , cmsInfoCount = 0
        }

    cmsincrby "cms1" (("foo", 2) NE.:| [("bar", 3), ("foo", 4)]) >>=? [2, 3, 6]
    cmsquery "cms1" ("foo" NE.:| ["bar", "baz"]) >>=? [6, 3, 0]
    cmsinfo "cms1" >>=? CMSInfo
        { cmsInfoWidth = 20
        , cmsInfoDepth = 5
        , cmsInfoCount = 9
        }

    cmsinitbyprob "cms2" 0.001 0.99 >>=? Ok
    cmsquery "cms2" ("foo" NE.:| ["bar"]) >>=? [0, 0]
    cmsincrby "cms2" (("foo", 7) NE.:| [("bar", 1)]) >>=? [7, 1]
    cmsinfo "cms2" >>@? \CMSInfo{..} -> do
        HUnit.assertBool "cms2 width should be positive" (cmsInfoWidth > 0)
        HUnit.assertBool "cms2 depth should be positive" (cmsInfoDepth > 0)
        8 HUnit.@=? cmsInfoCount

    cmsinitbydim "cms3" 20 5 >>=? Ok
    cmsincrby "cms3" (("foo", 7) NE.:| [("bar", 1)]) >>=? [7, 1]

    cmsinitbydim "cms_merged" 20 5 >>=? Ok
    cmsmerge "cms_merged" ("cms1" NE.:| ["cms3"]) >>=? Ok
    cmsquery "cms_merged" ("foo" NE.:| ["bar", "baz"]) >>=? [13, 4, 0]

    cmsinitbydim "cms_weighted" 20 5 >>=? Ok
    cmsmergeWeighted "cms_weighted" (("cms1", 1) NE.:| [("cms3", 2)]) >>=? Ok
    cmsquery "cms_weighted" ("foo" NE.:| ["bar", "baz"]) >>=? [20, 5, 0]

-- Top-K
--
testTopk :: Test
testTopk = testCase "topk" $ do
    topkReserve "topk1" 3 50 5 0.9 >>=? Ok
    topkInfo "topk1" >>=? TopkInfo
        { topkInfoK = 3
        , topkInfoWidth = 50
        , topkInfoDepth = 5
        , topkInfoDecay = 0.9
        }

    topkAdd "topk1" ("foo" NE.:| ["bar", "baz"]) >>=? [Nothing, Nothing, Nothing]
    topkQuery "topk1" ("foo" NE.:| ["bar", "missing"]) >>=? [True, True, False]
    topkCount "topk1" ("foo" NE.:| ["bar", "missing"]) >>@? \counts ->
        case counts of
            [fooCount, barCount, 0] -> do
                HUnit.assertBool "TOPK.COUNT foo should be positive" (fooCount > 0)
                HUnit.assertBool "TOPK.COUNT bar should be positive" (barCount > 0)
            _ -> HUnit.assertFailure $ "Unexpected TOPK.COUNT response: " ++ show counts

    topkIncrby "topk1" (("foo", 10) NE.:| [("baz", 3), ("qux", 1)]) >>@? \results ->
        HUnit.assertEqual "TOPK.INCRBY result length" 3 (length results)

    topkList "topk1" >>@? \items -> do
        HUnit.assertEqual "TOPK.LIST length" 3 (length items)
        HUnit.assertBool "TOPK.LIST should include foo" ("foo" `elem` items)

    topkListWithCount "topk1" >>@? \itemsWithCount -> do
        HUnit.assertEqual "TOPK.LIST WITHCOUNT length" 3 (length itemsWithCount)
        let itemKeys = map fst itemsWithCount
        HUnit.assertBool "TOPK.LIST WITHCOUNT should include foo" ("foo" `elem` itemKeys)
        HUnit.assertBool "TOPK.LIST WITHCOUNT counts should be positive" (all ((> 0) . snd) itemsWithCount)

-- T-Digest
--
testTdigest :: Test
testTdigest = testCase "tdigest" $ do
    tdigestCreate "td1" >>=? Ok
    tdigestAdd "td1" (1 NE.:| [2, 3, 4, 5]) >>=? Ok

    tdigestMin "td1" >>=? 1
    tdigestMax "td1" >>=? 5
    tdigestByrank "td1" (0 NE.:| [2, 4]) >>@? \values ->
        case values of
            [v0, v2, v4] -> do
                HUnit.assertEqual "TDIGEST.BYRANK 0" 1 v0
                HUnit.assertBool "TDIGEST.BYRANK 2 should be within range" (v2 >= 2 && v2 <= 4)
                HUnit.assertEqual "TDIGEST.BYRANK 4" 5 v4
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.BYRANK response: " ++ show values
    tdigestByrevrank "td1" (0 NE.:| [2, 4]) >>@? \values ->
        case values of
            [v0, v2, v4] -> do
                HUnit.assertEqual "TDIGEST.BYREVRANK 0" 5 v0
                HUnit.assertBool "TDIGEST.BYREVRANK 2 should be within range" (v2 >= 2 && v2 <= 4)
                HUnit.assertEqual "TDIGEST.BYREVRANK 4" 1 v4
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.BYREVRANK response: " ++ show values
    tdigestQuantile "td1" (0.0 NE.:| [0.5, 1.0]) >>@? \values ->
        case values of
            [q0, q50, q100] -> do
                HUnit.assertEqual "TDIGEST.QUANTILE 0" 1 q0
                HUnit.assertBool "TDIGEST.QUANTILE 0.5 should be within range" (q50 >= 2 && q50 <= 4)
                HUnit.assertEqual "TDIGEST.QUANTILE 1" 5 q100
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.QUANTILE response: " ++ show values

    tdigestCdf "td1" (1 NE.:| [3, 5]) >>@? \values ->
        case values of
            [cdf1, cdf3, cdf5] -> do
                HUnit.assertBool "TDIGEST.CDF should be monotonic" (cdf1 <= cdf3 && cdf3 <= cdf5)
                HUnit.assertBool "TDIGEST.CDF last value should be close to 1" (cdf5 >= 0.8)
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.CDF response: " ++ show values

    tdigestRank "td1" (1 NE.:| [3, 5]) >>@? \values ->
        case values of
            [r1, r3, r5] -> HUnit.assertBool "TDIGEST.RANK should be monotonic" (r1 <= r3 && r3 <= r5)
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.RANK response: " ++ show values

    tdigestRevrank "td1" (1 NE.:| [3, 5]) >>@? \values ->
        case values of
            [r1, r3, r5] -> HUnit.assertBool "TDIGEST.REVRANK should be monotonic descending" (r1 >= r3 && r3 >= r5)
            _ -> HUnit.assertFailure $ "Unexpected TDIGEST.REVRANK response: " ++ show values

    tdigestTrimmedMean "td1" 0.2 0.8 >>@? \value ->
        HUnit.assertBool "TDIGEST.TRIMMED_MEAN should be within observed range" (value >= 1 && value <= 5)

    tdigestInfo "td1" >>@? \TDigestInfo{..} -> do
        HUnit.assertBool "TDIGEST.INFO compression should be positive" (tdigestInfoCompression > 0)
        HUnit.assertBool "TDIGEST.INFO observations should reflect inserts" (tdigestInfoObservations >= 5)
        HUnit.assertBool "TDIGEST.INFO memory usage should be positive" (tdigestInfoMemoryUsage > 0)

    tdigestCreateOpts "td2" defaultTDigestCreateOpts
        { tdigestCreateCompression = Just 200
        } >>=? Ok
    tdigestAdd "td2" (10 NE.:| [20, 30]) >>=? Ok

    tdigestMerge "td_merged" ("td1" NE.:| ["td2"]) >>@? \status ->
        HUnit.assertEqual "TDIGEST.MERGE status" Ok status
    tdigestInfo "td_merged" >>@? \digestInfo ->
        HUnit.assertBool "TDIGEST.MERGE should populate destination" (tdigestInfoObservations digestInfo >= 8)

    tdigestCreate "td_override" >>=? Ok
    tdigestMergeOpts "td_override" ("td1" NE.:| ["td2"]) defaultTDigestMergeOpts
        { tdigestMergeCompression = Just 150
        , tdigestMergeOverride = True
        } >>=? Ok
    tdigestInfo "td_override" >>@? \digestInfo -> do
        150 HUnit.@=? tdigestInfoCompression digestInfo
        HUnit.assertBool "TDIGEST.MERGE override should populate destination" (tdigestInfoObservations digestInfo >= 8)

    tdigestReset "td1" >>=? Ok
    tdigestInfo "td1" >>@? \digestInfo ->
        HUnit.assertEqual "TDIGEST.RESET observations" 0 (tdigestInfoObservations digestInfo)

-- TimeSeries
--
testTs :: Test
testTs = testCase "timeseries" $ do
    tsCreateOpts "ts:series" defaultTsCreateOpts
        { tsCreateRetention = Just 100000
        , tsCreateEncoding = Just TsCompressed
        , tsCreateChunkSize = Just 128
        , tsCreateDuplicatePolicy = Just TsDuplicateLast
        , tsCreateLabels = [("metric", "temperature"), ("sensor", "alpha")]
        } >>=? Ok

    tsAdd "ts:series" "1000" 1.5 >>=? 1000
    tsAddOpts "ts:series" "2000" 2.5 defaultTsAddOpts
        { tsAddOnDuplicate = Just TsDuplicateLast
        } >>=? 2000
    tsGet "ts:series" >>=? Just (TsSample 2000 2.5)
    tsGetOpts "ts:series" defaultTsGetOpts
        { tsGetLatest = True
        } >>=? Just (TsSample 2000 2.5)

    tsRange "ts:series" "-" "+" >>=? [TsSample 1000 1.5, TsSample 2000 2.5]
    tsRangeOpts "ts:series" "-" "+" defaultTsRangeOpts
        { tsRangeCount = Just 1
        } >>=? [TsSample 1000 1.5]
    tsRevrange "ts:series" "-" "+" >>=? [TsSample 2000 2.5, TsSample 1000 1.5]

    tsAlter "ts:series" defaultTsAlterOpts
        { tsAlterRetention = Just 200000
        , tsAlterChunkSize = Just 256
        , tsAlterDuplicatePolicy = Just TsDuplicateMax
        , tsAlterLabels = [("metric", "temperature"), ("sensor", "beta")]
        } >>=? Ok

    tsIncrbyOpts "ts:counter" 5 defaultTsIncrByOpts
        { tsIncrByTimestamp = Just "1000"
        , tsIncrByLabels = [("metric", "counter"), ("sensor", "alpha")]
        } >>=? 1000
    tsDecrbyOpts "ts:counter" 2 defaultTsIncrByOpts
        { tsIncrByTimestamp = Just "2000"
        } >>=? 2000
    tsGet "ts:counter" >>=? Just (TsSample 2000 3.0)

    tsMadd
        ( ("ts:series", "3000", 3.5) NE.:|
          [ ("ts:counter", "3000", 4.0)
          ]
        ) >>=? [3000, 3000]

    tsQueryindex ("metric=temperature" NE.:| []) >>@? \seriesKeys ->
        HUnit.assertBool "TS.QUERYINDEX should return the labeled series" ("ts:series" `elem` seriesKeys)

    tsMget ("sensor=alpha" NE.:| []) >>@? \reply ->
        HUnit.assertBool "TS.MGET should return payload" (replyHasPayload reply)
    tsMgetOpts ("sensor=alpha" NE.:| []) defaultTsMGetOpts
        { tsMGetLatest = True
        , tsMGetLabels = Just TsWithLabels
        } >>@? \reply ->
            HUnit.assertBool "TS.MGET WITHLABELS should return payload" (replyHasPayload reply)

    tsMrange "-" "+" ("sensor=alpha" NE.:| []) >>@? \reply ->
        HUnit.assertBool "TS.MRANGE should return payload" (replyHasPayload reply)
    tsMrangeOpts "-" "+" ("sensor=alpha" NE.:| []) defaultTsMRangeOpts
        { tsMRangeLabels = Just (TsSelectedLabels ("metric" NE.:| ["sensor"]))
        , tsMRangeAggregation = Just TsAggregationOpts
            { tsAggregationAlign = Nothing
            , tsAggregationType = TsAggregators (TsAggAvg NE.:| [])
            , tsAggregationBucketDuration = 1000
            , tsAggregationBucketTimestamp = Just TsBucketStart
            , tsAggregationEmpty = False
            }
        } >>@? \reply ->
            HUnit.assertBool "TS.MRANGE aggregation should return payload" (replyHasPayload reply)
    tsMrevrange "-" "+" ("sensor=alpha" NE.:| []) >>@? \reply ->
        HUnit.assertBool "TS.MREVRANGE should return payload" (replyHasPayload reply)

    tsCreate "ts:source" >>=? Ok
    tsCreate "ts:dest" >>=? Ok
    tsCreaterule "ts:source" "ts:dest" TsAggAvg 1000 >>=? Ok
    tsAdd "ts:source" "1000" 1 >>=? 1000
    tsAdd "ts:source" "2000" 3 >>=? 2000
    tsAdd "ts:source" "3000" 5 >>=? 3000
    delRuleResult <- tsDelrule "ts:source" "ts:dest"
    liftIO $ case delRuleResult of
        Right Ok -> pure ()
        Left reply | isUnknownCommandReply reply -> pure ()
        Left reply -> HUnit.assertFailure $ "Unexpected TS.DELRULE reply: " ++ show reply
        Right status -> HUnit.assertFailure $ "Unexpected TS.DELRULE status: " ++ show status

    tsInfo "ts:series" >>@? \reply ->
        HUnit.assertBool "TS.INFO should return payload" (replyHasPayload reply)
    tsInfoOpts "ts:series" TsInfoDebug >>@? \reply ->
        HUnit.assertBool "TS.INFO DEBUG should return payload" (replyHasPayload reply)

    tsDel "ts:series" 3000 3000 >>=? 1
    tsRange "ts:series" "-" "+" >>=? [TsSample 1000 1.5, TsSample 2000 2.5]
  where
    replyHasPayload = \case
        Bulk (Just value) -> not (BS.null value)
        MultiBulk (Just replies) -> not (null replies)
        Integer _ -> True
        SingleLine value -> not (BS.null value)
        Error _ -> False
        Bulk Nothing -> False
        MultiBulk Nothing -> False

-- RedisJSON
--
testJSON :: Test
testJSON = testCase "json" $ do
    jsonSet "json:doc" "$"
        "{\"name\":\"base\",\"numbers\":[1,2],\"enabled\":true,\"nested\":{\"inner\":5},\"remove\":\"gone\"}" >>=? Just Ok

    jsonGet "json:doc" >>@? \actual ->
        case actual of
            Just payload -> HUnit.assertBool "JSON.GET should return the object" ("\"name\":\"base\"" `BS.isInfixOf` payload)
            Nothing -> HUnit.assertFailure "JSON.GET returned Nothing"

    jsonGetOpts "json:doc" defaultJSONGetOpts
        { jsonGetIndent = Just "  "
        , jsonGetNewline = Just "\n"
        , jsonGetSpace = Just " "
        , jsonGetPaths = ["$"]
        } >>@? \actual ->
            case actual of
                Just payload -> HUnit.assertBool "JSON.GET opts should format output" ("\n" `BS.isInfixOf` payload)
                Nothing -> HUnit.assertFailure "JSON.GET opts returned Nothing"

    jsonSetOpts "json:nx" "$" "{\"value\":1}" defaultJSONSetOpts
        { jsonSetCondition = Just JSONSetIfNotExists
        } >>=? Just Ok
    jsonSetOpts "json:nx" "$" "{\"value\":2}" defaultJSONSetOpts
        { jsonSetCondition = Just JSONSetIfNotExists
        } >>=? Nothing
    jsonSetOpts "json:nx" "$" "{\"value\":3}" defaultJSONSetOpts
        { jsonSetCondition = Just JSONSetIfExists
        } >>=? Just Ok

    jsonMset
        ( ("json:m1", "$", "{\"name\":\"one\"}") NE.:|
          [ ("json:m2", "$", "{\"name\":\"two\"}")
          ]
        ) >>=? Ok
    jsonMget ("json:m1" NE.:| ["json:m2"]) "$" >>@? \results ->
        HUnit.assertBool "JSON.MGET should return both values" (length results == 2 && all isJust results)

    jsonArrappend "json:doc" "$.numbers" ("3" NE.:| ["4"]) >>@? \reply ->
        HUnit.assertEqual "JSON.ARRAPPEND result" [4] (replyIntegers reply)
    jsonArrlenAt "json:doc" "$.numbers" >>@? \reply ->
        HUnit.assertEqual "JSON.ARRLEN result" [4] (replyIntegers reply)
    jsonArrindexOpts "json:doc" "$.numbers" "2" (JSONArrIndexFromTo 0 3) >>@? \reply ->
        HUnit.assertEqual "JSON.ARRINDEX result" [1] (replyIntegers reply)
    jsonArrinsert "json:doc" "$.numbers" 1 ("9" NE.:| []) >>@? \reply ->
        HUnit.assertEqual "JSON.ARRINSERT result" [5] (replyIntegers reply)
    jsonArrpopAtIndex "json:doc" "$.numbers" 1 >>@? \reply ->
        HUnit.assertBool "JSON.ARRPOP should return popped value" (replyContains "9" reply)
    jsonArrtrim "json:doc" "$.numbers" 1 2 >>@? \reply ->
        HUnit.assertEqual "JSON.ARRTRIM result" [2] (replyIntegers reply)

    jsonNumincrby "json:doc" "$.nested.inner" 5 >>@? \reply ->
        HUnit.assertBool "JSON.NUMINCRBY should update value" (replyContains "10" reply)
    jsonNummultby "json:doc" "$.nested.inner" 2 >>@? \reply ->
        HUnit.assertBool "JSON.NUMMULTBY should update value" (replyContains "20" reply)
    jsonClearAt "json:doc" "$.nested.inner" >>=? 1
    jsonGetOpts "json:doc" defaultJSONGetOpts { jsonGetPaths = ["$.nested.inner"] } >>@? \actual ->
        case actual of
            Just payload -> HUnit.assertBool "JSON.CLEAR should zero numeric value" ("0" `BS.isInfixOf` payload)
            Nothing -> HUnit.assertFailure "JSON.GET nested inner returned Nothing"

    jsonStrappendAt "json:doc" "$.name" "\"!\"" >>@? \reply ->
        HUnit.assertEqual "JSON.STRAPPEND result" [5] (replyIntegers reply)
    jsonToggle "json:doc" "$.enabled" >>@? \reply ->
        HUnit.assertEqual "JSON.TOGGLE result" [False] (replyBools reply)

    jsonObjkeysAt "json:doc" "$" >>@? \reply ->
        HUnit.assertBool "JSON.OBJKEYS should include name" (replyContains "name" reply)
    jsonObjlenAt "json:doc" "$" >>@? \reply ->
        HUnit.assertEqual "JSON.OBJLEN result" [5] (replyIntegers reply)
    jsonTypeAt "json:doc" "$.numbers" >>@? \reply ->
        HUnit.assertBool "JSON.TYPE should report array" (replyContains "array" reply)
    jsonRespAt "json:doc" "$.name" >>@? \reply ->
        HUnit.assertBool "JSON.RESP should return payload" (replyHasPayload reply)
    jsonDebugMemoryAt "json:doc" "$.numbers" >>@? \reply ->
        HUnit.assertBool "JSON.DEBUG MEMORY should return payload" (replyHasPayload reply)

    jsonMerge "json:doc" "$" "{\"merged\":true,\"name\":\"base!\"}" >>=? Ok
    jsonGet "json:doc" >>@? \actual ->
        case actual of
            Just payload -> do
                HUnit.assertBool "JSON.MERGE should add merged field" ("\"merged\":true" `BS.isInfixOf` payload)
                HUnit.assertBool "JSON.MERGE should update name" ("\"name\":\"base!\"" `BS.isInfixOf` payload)
            Nothing -> HUnit.assertFailure "JSON.GET after merge returned Nothing"

    jsonDelAt "json:doc" "$.remove" >>=? 1
    jsonForgetAt "json:doc" "$.merged" >>=? 1
    jsonGet "json:doc" >>@? \actual ->
        case actual of
            Just payload -> do
                HUnit.assertBool "JSON.DEL should remove field" (not ("\"remove\"" `BS.isInfixOf` payload))
                HUnit.assertBool "JSON.FORGET should remove field" (not ("\"merged\"" `BS.isInfixOf` payload))
            Nothing -> HUnit.assertFailure "JSON.GET after delete/forget returned Nothing"

    jsonSet "json:rootArray" "$" "[1,2,3]" >>=? Just Ok
    jsonArrlen "json:rootArray" >>@? \reply ->
        HUnit.assertBool "JSON.ARRLEN root should return payload" (replyHasPayload reply)
    jsonArrpop "json:rootArray" >>@? \reply ->
        HUnit.assertBool "JSON.ARRPOP root should return payload" (replyContains "3" reply)

    jsonSet "json:rootString" "$" "\"abc\"" >>=? Just Ok
    jsonStrappend "json:rootString" "\"d\"" >>@? \reply ->
        HUnit.assertBool "JSON.STRAPPEND root should return payload" (replyHasPayload reply)

    jsonSet "json:clearRoot" "$" "{\"a\":1}" >>=? Just Ok
    jsonClear "json:clearRoot" >>=? 1

    jsonSet "json:deleteRoot" "$" "{\"a\":1}" >>=? Just Ok
    jsonForget "json:deleteRoot" >>=? 1

    jsonSet "json:typeRoot" "$" "{\"a\":1}" >>=? Just Ok
    jsonType "json:typeRoot" >>@? \reply ->
        HUnit.assertBool "JSON.TYPE root should return payload" (replyHasPayload reply)
    jsonObjkeys "json:typeRoot" >>@? \reply ->
        HUnit.assertBool "JSON.OBJKEYS root should return payload" (replyContains "a" reply)
    jsonObjlen "json:typeRoot" >>@? \reply ->
        HUnit.assertBool "JSON.OBJLEN root should return payload" (replyHasPayload reply)
    jsonResp "json:typeRoot" >>@? \reply ->
        HUnit.assertBool "JSON.RESP root should return payload" (replyHasPayload reply)
    jsonDebugMemory "json:typeRoot" >>@? \reply ->
        HUnit.assertBool "JSON.DEBUG MEMORY root should return payload" (replyHasPayload reply)
    jsonDel "json:typeRoot" >>=? 1
  where
    isJust (Just _) = True
    isJust Nothing = False

    replyContains needle = \case
        SingleLine value -> needle `BS.isInfixOf` value
        Error value -> needle `BS.isInfixOf` value
        Integer value -> needle `BS.isInfixOf` Char8.pack (show value)
        Bulk (Just value) -> needle `BS.isInfixOf` value
        Bulk Nothing -> False
        MultiBulk (Just replies) -> any (replyContains needle) replies
        MultiBulk Nothing -> False

    replyHasPayload = \case
        Bulk (Just value) -> not (BS.null value)
        MultiBulk (Just replies) -> not (null replies)
        Integer _ -> True
        SingleLine value -> not (BS.null value)
        Error _ -> False
        Bulk Nothing -> False
        MultiBulk Nothing -> False

    replyIntegers = \case
        Integer value -> [value]
        MultiBulk (Just replies) -> [value | reply <- replies, Right value <- [decode reply :: Either Reply Integer]]
        reply -> case decode reply :: Either Reply Integer of
            Right value -> [value]
            Left _ -> []

    replyBools = \case
        Integer 1 -> [True]
        Integer 0 -> [False]
        MultiBulk (Just replies) -> [value | reply <- replies, Right value <- [decode reply :: Either Reply Bool]]
        reply -> case decode reply :: Either Reply Bool of
            Right value -> [value]
            Left _ -> []

------------------------------------------------------------------------------
-- Cuckoo Filters
--
testCuckooFilter :: Test
testCuckooFilter = testCase "cuckoo filter" $ do
    cfreserveOpts "cf" 1000 defaultCFReserveOpts
        { cfReserveBucketSize = Just 4
        , cfReserveMaxIterations = Just 50
        , cfReserveExpansion = Just 2
        } >>=? Ok

    cfinfo "cf" >>@? \CFInfo{..} -> do
        HUnit.assertBool "cfInfoSize should be positive" (cfInfoSize > 0)
        HUnit.assertBool "cfInfoBuckets should be positive" (cfInfoBuckets > 0)
        1 HUnit.@=? cfInfoFilters
        0 HUnit.@=? cfInfoItemsInserted
        0 HUnit.@=? cfInfoItemsDeleted
        4 HUnit.@=? cfInfoBucketSize
        2 HUnit.@=? cfInfoExpansion
        50 HUnit.@=? cfInfoMaxIterations

    cfadd "cf" "item1" >>=? True
    cfadd "cf" "item1" >>=? True
    cfcount "cf" "item1" >>=? 2
    cfexists "cf" "item1" >>=? True
    cfaddnx "cf" "item1" >>=? False
    cfcount "cf" "item1" >>=? 2

    cfinsert "cf" ("item2" NE.:| ["item3"]) >>=? [CFInsertAdded, CFInsertAdded]
    cfinsertnx "cf" ("item1" NE.:| ["item4"]) >>=? [CFInsertAlreadyExists, CFInsertAdded]
    cfmexists "cf" ("item1" NE.:| ["item2", "item3", "item4", "missing"]) >>=? [True, True, True, True, False]

    cfdel "cf" "item2" >>=? True
    cfcount "cf" "item2" >>=? 0
    cfdel "cf" "item2" >>=? False

------------------------------------------------------------------------------
-- Pub/Sub
--
testPubSub :: Test
testPubSub conn = testCase "pubSub" go conn
  where
    go = do
        -- producer
        asyncProducer <- liftIO $ Async.async $ do
            runRedis conn $ do
                let t = 10^(5 :: Int)
                liftIO $ threadDelay t
                publish "chan1" "hello" >>=? 1
                liftIO $ threadDelay t
                publish "chan2" "world" >>=? 1
            return ()

        -- consumer
        pubSub (subscribe ["chan1"]) $ \msg -> do
            -- ready for a message
            case msg of
                Message{..} -> return
                    (unsubscribe [msgChannel] `mappend` psubscribe ["chan*"])
                PMessage{..} -> return (punsubscribe [msgPattern])

        pubSub (subscribe [] `mappend` psubscribe []) $ \_ -> do
            liftIO $ HUnit.assertFailure "no subs: should return immediately"
            undefined
        liftIO $ Async.wait asyncProducer


------------------------------------------------------------------------------
-- Transaction
--
testTransaction :: Test
testTransaction = testCase "transaction" $ do
    watch ["{same}k1", "{same}k2"] >>=? Ok
    unwatch            >>=? Ok
    set "{same}foo" "foo" >>= \case
      Left _ -> error "error"
      _ -> return ()
    set "{same}bar" "bar" >>= \case
      Left _ -> error "error"
      _ -> return ()
    foobar <- multiExec $ do
        foo <- get "{same}foo"
        bar <- get "{same}bar"
        return $ (,) <$> foo <*> bar
    assert $ foobar == TxSuccess (Just "foo", Just "bar")

testSet7 :: Test
testSet7 = testCase "Set" $ do
    set "hello" "hi" >>=? Ok
    setOpts "hello" "hi" SetOpts{
        setSeconds           = Nothing,
        setMilliseconds      = Nothing,
        setUnixSeconds       = Just 2000,
        setUnixMilliseconds  = Nothing,
        setCondition         = Nothing,
        setKeepTTL           = False
    } >>=? Ok
    setOpts "hello" "hi" SetOpts{
        setSeconds           = Nothing,
        setMilliseconds      = Nothing,
        setUnixSeconds       = Nothing,
        setUnixMilliseconds  = Just 20000,
        setCondition         = Nothing,
        setKeepTTL           = False
    } >>=? Ok
    setOpts "hello" "hi" SetOpts{
        setSeconds           = Nothing,
        setMilliseconds      = Nothing,
        setUnixSeconds       = Nothing,
        setUnixMilliseconds  = Nothing,
        setCondition         = Nothing,
        setKeepTTL           = True
    } >>=? Ok
    setGet "hello" "henlo" >>=? "hi"
    setGetOpts "hello" "henlo2" SetOpts{
        setSeconds           = Nothing,
        setMilliseconds      = Nothing,
        setUnixSeconds       = Nothing,
        setUnixMilliseconds  = Nothing,
        setCondition         = Just Nx,
        setKeepTTL           = False
    } >>=? "henlo"
    return ()

testZAdd7 :: Test
testZAdd7 = testCase "ZADD" $ do
    zadd "set" [(42, "2")] >>=? 1
    zaddOpts "set" [(44, "6")] (defaultZaddOpts {zaddSizeCondition = Just CGT}) >>=? 1
    zaddOpts "set" [(46, "7")] (defaultZaddOpts {zaddSizeCondition = Just CLT}) >>=? 1
    return ()

testExpireTime7 :: Test
testExpireTime7 = testCase "expiretime" $ do
    set "mykey" "Hello" >>=? Ok
    expireat "mykey" 33177117420 >>=? True
    expiretime "mykey" >>=? 33177117420
    pexpireat "mykey" 33177117420000 >>=? True
    pexpiretime "mykey" >>=? 33177117420000

testHashExpire7 :: Test
testHashExpire7 = testCase "hash expire" $ do
    hset "mykey" [("field1", "hello"), ("field2", "world")] >>=? 2

    hexpire "mykey" 60 ("field1" NE.:| ["field2", "missing"]) >>=?
        [ HashFieldExpirationSet
        , HashFieldExpirationSet
        , HashFieldExpirationNoSuchField
        ]

    httl "mykey" ("field1" NE.:| ["field2", "missing"]) >>@? \values ->
        case values of
            [HashFieldExpirationInfo ttl1, HashFieldExpirationInfo ttl2, HashFieldExpirationInfoNoSuchField] -> do
                HUnit.assertBool "HTTL field1 should be positive" (ttl1 > 0 && ttl1 <= 60)
                HUnit.assertBool "HTTL field2 should be positive" (ttl2 > 0 && ttl2 <= 60)
            _ -> HUnit.assertFailure $ "Unexpected HTTL reply: " ++ show values

    hpexpire "mykey" 2000 ("field1" NE.:| ["field2"]) >>=?
        [ HashFieldExpirationSet
        , HashFieldExpirationSet
        ]

    hpttl "mykey" ("field1" NE.:| ["field2"]) >>@? \values ->
        case values of
            [HashFieldExpirationInfo ttl1, HashFieldExpirationInfo ttl2] -> do
                HUnit.assertBool "HPTTL field1 should be positive" (ttl1 > 0 && ttl1 <= 2000)
                HUnit.assertBool "HPTTL field2 should be positive" (ttl2 > 0 && ttl2 <= 2000)
            _ -> HUnit.assertFailure $ "Unexpected HPTTL reply: " ++ show values

    now <- round <$> liftIO getPOSIXTime
    hexpireat "mykey" (now + 60) ("field1" NE.:| ["field2"]) >>=?
        [ HashFieldExpirationSet
        , HashFieldExpirationSet
        ]

    hexpiretime "mykey" ("field1" NE.:| ["field2"]) >>@? \values ->
        case values of
            [HashFieldExpirationInfo ts1, HashFieldExpirationInfo ts2] -> do
                HUnit.assertBool "HEXPIRETIME field1 should be near target" (ts1 >= now && ts1 <= now + 60)
                HUnit.assertBool "HEXPIRETIME field2 should be near target" (ts2 >= now && ts2 <= now + 60)
            _ -> HUnit.assertFailure $ "Unexpected HEXPIRETIME reply: " ++ show values

    nowMs <- round . (* 1000) <$> liftIO getPOSIXTime
    hpexpireat "mykey" (nowMs + 2000) ("field1" NE.:| ["field2"]) >>=?
        [ HashFieldExpirationSet
        , HashFieldExpirationSet
        ]

    hpexpiretime "mykey" ("field1" NE.:| ["field2"]) >>@? \values ->
        case values of
            [HashFieldExpirationInfo ts1, HashFieldExpirationInfo ts2] -> do
                HUnit.assertBool "HPEXPIRETIME field1 should be near target" (ts1 >= nowMs && ts1 <= nowMs + 2000)
                HUnit.assertBool "HPEXPIRETIME field2 should be near target" (ts2 >= nowMs && ts2 <= nowMs + 2000)
            _ -> HUnit.assertFailure $ "Unexpected HPEXPIRETIME reply: " ++ show values

    hexpireOpts "mykey" 10 ("field1" NE.:| [])
        (ExpireOptsTime Nx)
        >>=? [HashFieldExpirationConditionNotMet]

testSintercard7 :: Test
testSintercard7 = testCase "sintercard" $ do
    sadd "{same}bikes:racing:france" ("bike:1" NE.:| ["bike:2", "bike:3"]) >>=? 3
    sadd "{same}bikes:racing:usa" ("bike:1" NE.:| ["bike:4"]) >>=? 2
    sadd "{same}bikes:racing:japan" ("bike:1" NE.:| ["bike:3"]) >>=? 2
    sintercard ("{same}bikes:racing:france" NE.:| ["{same}bikes:racing:usa", "{same}bikes:racing:japan"]) >>=? 1
    sintercardOpts ("{same}bikes:racing:france" NE.:| ["{same}bikes:racing:usa", "{same}bikes:racing:japan"])
        defaultSintercardOpts { sintercardLimit = Just 1 } >>=? 1

testLMPop7 :: Test
testLMPop7 = testCase "lmpop" $ do
    lmpop ("non1" NE.:| ["non2"]) ListLeft >>=? Nothing
    lpush "mylist" ["one", "two", "three", "four", "five"] >>=? 5
    lmpop ("mylist" NE.:| []) ListLeft >>=? Just ("mylist", ["five"])
    lrange "mylist" 0 (-1) >>=? ["four", "three", "two", "one"]

    lpush "mylist2" ["a", "b", "c", "d", "e"] >>=? 5
    lpush "mylist3" ["one", "two", "three", "four", "five"] >>=? 5
    lmpopCount ("mylist" NE.:| ["mylist2"]) ListRight 3 >>=? Just ("mylist", ["one", "two", "three"])
    blmpopCount 1 ("mylist3" NE.:| ["mylist2"]) ListRight 5 >>=? Just ("mylist3", ["one", "two", "three", "four", "five"])
    blmpopCount 1 ("mylist3" NE.:| ["mylist2"]) ListRight 10 >>=? Just ("mylist2", ["a", "b", "c", "d", "e"])

testZMPop7 :: Test
testZMPop7 = testCase "zmpop" $ do
    zadd "{same}zset1" [(1, "one"), (2, "two"), (3, "three")] >>=? 3
    zadd "{same}zset2" [(10, "ten")] >>=? 1
    zmpop ("{same}zset1" NE.:| ["{same}zset2"]) ZPopMax >>=? Just ZPopResponse
        { zPopResponseKey = Just "{same}zset1"
        , zPopResponseValues = [("three", 3)]
        }
    zmpopCount ("{same}zset1" NE.:| ["{same}zset2"]) ZPopMin 2 >>=? Just ZPopResponse
        { zPopResponseKey = Just "{same}zset1"
        , zPopResponseValues = [("one", 1), ("two", 2)]
        }
    bzmpopCount 1 ("{same}zset1" NE.:| ["{same}zset2"]) ZPopMax 2 >>=? Just ZPopResponse
        { zPopResponseKey = Just "{same}zset2"
        , zPopResponseValues = [("ten", 10)]
        }

testFunction7 :: Test
testFunction7 = testCase "function" $ do
    functionFlushOpts FlushOptsSync >>=? Ok

    let libraryCode = "#!lua name=mylib\nredis.register_function('myfunc', function(keys, args) return args[1] end)\nredis.register_function{function_name='myro', callback=function(keys, args) return redis.call('GET', keys[1]) end, flags={ 'no-writes' }}"

    functionLoad libraryCode >>=? "mylib"
    fcall "myfunc" [] ["hello"] >>=? ("hello" :: ByteString)
    set "mykey" "value" >>=? Ok
    fcallReadonly "myro" ["mykey"] [] >>=? ("value" :: ByteString)
    functionList >>@? \reply ->
        case reply of
            MultiBulk (Just _) -> pure ()
            _ -> HUnit.assertFailure $ "Unexpected FUNCTION LIST reply: " ++ show reply
    functionListOpts defaultFunctionListOpts { functionListLibraryName = Just "mylib", functionListWithCode = True } >>@? \reply ->
        case reply of
            MultiBulk (Just _) -> pure ()
            _ -> HUnit.assertFailure $ "Unexpected FUNCTION LIST WITHCODE reply: " ++ show reply
    functionStats >>@? \reply ->
        case reply of
            MultiBulk (Just _) -> pure ()
            _ -> HUnit.assertFailure $ "Unexpected FUNCTION STATS reply: " ++ show reply
    payload <- functionDump
    case payload of
        Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected FUNCTION DUMP reply: " ++ show reply
        Right dumped -> do
            functionDelete "mylib" >>=? Ok
            functionRestore dumped >>=? Ok
            fcall "myfunc" [] ["restored"] >>=? ("restored" :: ByteString)
            functionDelete "mylib" >>=? Ok
            functionRestoreOpts dumped (FunctionRestoreWithPolicy FunctionRestoreAppend) >>=? Ok
            fcall "myfunc" [] ["restored-again"] >>=? ("restored-again" :: ByteString)
    killResult <- functionKill
    liftIO $ case killResult of
        Left (Error _) -> pure ()
        Left reply -> HUnit.assertFailure $ "Unexpected FUNCTION KILL reply: " ++ show reply
        Right status -> HUnit.assertFailure $ "Unexpected FUNCTION KILL status: " ++ show status
    functionFlushOpts FlushOptsSync >>=? Ok

testCommandList7 :: Test
testCommandList7 = testCase "command list" $ do
    commandList >>@? \commands ->
        HUnit.assertBool "COMMAND LIST should contain GET" ("get" `elem` commands)
    commandListOpts (Just $ CommandListFilterByPattern "x*") >>@? \commands -> do
        HUnit.assertBool "pattern-filtered command list should contain xadd" ("xadd" `elem` commands)
        HUnit.assertBool "pattern-filtered command list should exclude get" ("get" `notElem` commands)
    commandListOpts (Just $ CommandListFilterByAclCat "connection") >>@? \commands ->
        HUnit.assertBool "ACLCAT-filtered command list should contain ping" ("ping" `elem` commands)

------------------------------------------------------------------------------
-- Scripting
--
testScripting :: Test
testScripting conn = testCase "scripting" go conn
  where
    go = do
        let script    = "return {false, 42}"
            scriptRes = (False, 42 :: Integer)
        scriptLoad script >>= \case
          Left _ -> error "error"
          Right scriptHash -> do
            eval script [] []                       >>=? scriptRes
            evalsha scriptHash [] []                >>=? scriptRes
            scriptExists [scriptHash, "notAScript"] >>=? [True, False]
            scriptFlush                             >>=? Ok
            -- start long running script from another client
            configSet "lua-time-limit" "100"        >>=? Ok
            evalFinished <- liftIO newEmptyMVar
            asyncScripting <- liftIO $ Async.async $ runRedis conn $ do
                -- we must pattern match to block the thread
                (eval "while true do end" [] []
                    :: Redis (Either Reply Integer)) >>= \case
                    Left _ -> return ()
                    _ -> error "impossible"
                liftIO (putMVar evalFinished ())
                return ()
            liftIO (threadDelay 500000) -- 0.5s
            scriptKill                              >>=? Ok
            () <- liftIO (takeMVar evalFinished)
            liftIO $ Async.wait asyncScripting
            return ()

------------------------------------------------------------------------------
-- Connection
--
testConnectAuth :: String -> PortNumber -> Test
testConnectAuth host port = testCase "connect/auth" $ do
    configSet "requirepass" "pass" >>=? Ok
    liftIO $ do
        c <- checkedConnect defaultConnectInfo { connectAuth = Just "pass", connectAddr = ConnectAddrHostPort host port }
        runRedis c (ping >>=? Pong)
    auth "pass"                    >>=? Ok
    configSet "requirepass" ""     >>=? Ok

testConnectAuthUnexpected :: String -> PortNumber -> Test
testConnectAuthUnexpected host port = testCase "connect/auth/unexpected" $ do
    liftIO $ do
        res <- try $ void $ checkedConnect connInfo
        HUnit.assertEqual "" err res

    where connInfo = defaultConnectInfo { connectAuth = Just "pass", connectAddr = ConnectAddrHostPort host port }
          err = Left $ ConnectAuthError $
                  Error "ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?"


testConnectAuthAcl :: String -> PortNumber -> Test
testConnectAuthAcl host port = testCase "connect/auth/acl" $ do
   liftIO $ do
      c <- checkedConnect defaultConnectInfo { connectAddr = ConnectAddrHostPort host port }
      runRedis c $ sendRequest  ["ACL", "SETUSER", "test", "on", ">pass", "~*", "&*", "+@all"] >>=? Ok
   liftIO $ do
      c <- checkedConnect defaultConnectInfo{connectAuth=Just "pass", connectUsername=Just "test", connectAddr = ConnectAddrHostPort host port}
      runRedis c (ping >>=? Pong)
   liftIO $ do
      res <- try $ void $ checkedConnect defaultConnectInfo{connectAuth=Just "pass", connectUsername=Just "test1", connectAddr = ConnectAddrHostPort host port}
      HUnit.assertEqual "" err res
   where
     err = Left $ ConnectAuthError $
             Error "WRONGPASS invalid username-password pair or user is disabled."

testConnectDb :: String -> PortNumber -> Test
testConnectDb host port = testCase "connect/db" $ do
    set "connect" "value" >>=? Ok
    liftIO $ void $ do
        c <- checkedConnect defaultConnectInfo { connectDatabase = 1, connectAddr = ConnectAddrHostPort host port }
        runRedis c (get "connect" >>=? Nothing)

testConnectDbUnexisting :: String -> PortNumber -> Test
testConnectDbUnexisting host port = testCase "connect/db/unexisting" $ do
    liftIO $ do
        res <- try $ void $ checkedConnect connInfo
        case res of
          Left (ConnectSelectError _) -> return ()
          _ -> HUnit.assertFailure $
                  "Expected ConnectSelectError, got " ++ show res

    where connInfo = defaultConnectInfo { connectDatabase = 100, connectAddr = ConnectAddrHostPort host port }

testClientUnpause :: Test
testClientUnpause = testCase "client/unpause" $
    clientUnpause >>=? Ok

testEcho :: Test
testEcho = testCase "echo" $
    echo ("value" ) >>=? "value"

testPing :: Test
testPing = testCase "ping" $ ping >>=? Pong

testQuit :: Test
testQuit = testCase "quit" $ quit >>=? Ok

testSelect :: Test
testSelect = testCase "select" $ do
    select 13 >>=? Ok
    select 0 >>=? Ok


------------------------------------------------------------------------------
-- Client
--
testClientId :: Test
testClientId = testCase "client id" $ do
    clientId >>= assert . isRight

testClientName :: Test
testClientName = testCase "client {get,set}name" $ do
    clientGetname >>=? Nothing
    clientSetname "FooBar" >>=? Ok
    clientGetname >>=? Just "FooBar"


------------------------------------------------------------------------------
-- Server
--
testServer :: Test
testServer = testCase "server" $ do
    time >>= \case
      Right (_,_) -> return ()
      Left _ -> error "error"
    slaveof "no" "one" >>=? Ok
    return ()

testBgrewriteaof :: Test
testBgrewriteaof = testCase "bgrewriteaof/bgsave/save" $ do
    save >>=? Ok
    bgsave >>= \case
      Right (Status _) -> return ()
      _ -> error "error"
    -- Redis needs time to finish the bgsave
    liftIO $ threadDelay (10^(5 :: Int))
    bgrewriteaof >>= \case
      Right (Status _) -> return ()
      _ -> error "error"
    return ()

testConfig :: Test
testConfig = testCase "config/auth" $ do
    configGet ["requirepass"]      >>=? [("requirepass", "")]
    configSet "requirepass" "pass" >>=? Ok
    auth "pass"                    >>=? Ok
    configSet "requirepass" ""     >>=? Ok

testFlushall :: Test
testFlushall = testCase "flushall/flushdb" $ do
    flushall >>=? Ok
    flushdb  >>=? Ok

testInfo :: Test
testInfo = testCase "info/lastsave/dbsize" $ do
    info >>= \case
      Left _ -> error "error"
      _ -> return ()
    lastsave >>= \case
      Left _ -> error "error"
      _ -> return ()
    dbsize          >>=? 0
    configResetstat >>=? Ok

testSlowlog :: Test
testSlowlog = testCase "slowlog" $ do
    slowlogReset >>=? Ok
    slowlogGet 5 >>=? []
    slowlogLen   >>=? 0

-- |Starting with Redis 7.0.0, the DEBUG command is disabled by default and must be enabled manually in the Redis Config file
testDebugObject :: Test
testDebugObject = testCase "debugObject/debugSegfault" $ do
    return ()
    -- set "key" "value" >>=? Ok
    -- debugObject "key" >>= \case
      -- Left _ -> error "error"
      -- _ -> return ()
    -- return ()

testScans :: Test
testScans = testCase "scans" $ do
    set "key" "value"       >>=? Ok
    scan cursor0            >>=? (cursor0, ["key"])
    scanOpts cursor0 sOpts1 Nothing >>=? (cursor0, ["key"])
    scanOpts cursor0 sOpts2 Nothing >>=? (cursor0, [])
    where sOpts1 = defaultScanOpts { scanMatch = Just "k*" }
          sOpts2 = defaultScanOpts { scanMatch = Just "not*"}

testSScan :: Test
testSScan = testCase "sscan" $ do
    sadd "set" (NE.fromList ["1"]) >>=? 1
    sscan "set" cursor0     >>=? (cursor0, ["1"])

testHScan :: Test
testHScan = testCase "hscan" $ do
    hset "hash" [("k"::ByteString, "v"::ByteString)] >>=? 1
    hscan "hash" cursor0     >>=? (cursor0, [("k", "v")])

testZScan :: Test
testZScan = testCase "zscan" $ do
    zadd "zset" [(42, "2")] >>=? 1
    zscan "zset" cursor0    >>=? (cursor0, [("2", 42)])

testZrangelex ::Test
testZrangelex = testCase "zrangebylex" $ do
    let testSet = [(10, "aaa"), (10, "abb"), (10, "ccc"), (10, "ddd")]
    zadd "zrangebylex" testSet                          >>=? 4
    zrangebylex "zrangebylex" (Incl "aaa") (Incl "bbb") >>=? ["aaa","abb"]
    zrangebylex "zrangebylex" (Excl "aaa") (Excl "ddd") >>=? ["abb","ccc"]
    zrangebylex "zrangebylex" Minr Maxr                 >>=? ["aaa","abb","ccc","ddd"]
    zrangebylexLimit "zrangebylex" Minr Maxr 2 1        >>=? ["ccc"]

testXAddRead ::Test
testXAddRead = testCase "xadd/xread" $ do
    xadd "{same}somestream8" "123" [("key", "value"), ("key2", "value2")]
    xadd "{same}otherstream" "456" [("key1", "value1")]
    xaddOpts "{same}thirdstream" "*" [("k", "v")]
        $ xaddTrimOpt (Just $ trimOpts (TrimMaxlen 1) TrimExact)
    xaddOpts "{same}thirdstream" "*" [("k", "v")]
        $ xaddTrimOpt (Just $ trimOpts (TrimMaxlen 1) (TrimApprox Nothing))
    xread [("{same}somestream8", "0"), ("{same}otherstream", "0")] >>=? Just [
        XReadResponse {
            stream = "{same}somestream8",
            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value"), ("key2", "value2")]}]
        },
        XReadResponse {
            stream = "{same}otherstream",
            records = [StreamsRecord{recordId = "456-0", keyValues = [("key1", "value1")]}]
        }]
    xlen "{same}somestream8" >>=? 1
    where xaddTrimOpt a = XAddOpts{
        xAddTrimOpts = a,
        xAddnoMkStream = False}

testXReadGroup ::Test
testXReadGroup = testCase "XGROUP */xreadgroup/xack" $ void $ runExceptT $ do
    ExceptT $ xadd "somestream8" "123" [("key", "value")]
    ExceptT $ xgroupCreate "somestream8" "somegroup" "0"
    readResult <- ExceptT $ xreadGroup "somegroup" "consumer1" [("somestream8", ">")]
    liftIO $ readResult HUnit.@=? Just [
        XReadResponse {
            stream = "somestream8",
            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value")]}]
        }]
    noAcked <- ExceptT $ xack "somestream8" "somegroup" ["123-0"]
    liftIO $ noAcked HUnit.@=? 1
    groupMessages <- ExceptT $ xreadGroup "somegroup" "consumer1" [("somestream8", ">")]
    liftIO $ groupMessages HUnit.@=? Nothing
    setIdOk <- ExceptT $ xgroupSetId "somestream8" "somegroup" "0"
    liftIO $ setIdOk HUnit.@=? Ok
    itemsLeft <- ExceptT $ xgroupDelConsumer "somestream8" "somegroup" "consumer1"
    liftIO $ itemsLeft HUnit.@=? 0
    groupDestroyed <- ExceptT (xgroupDestroy "somestream8" "somegroup")
    liftIO $ groupDestroyed HUnit.@=? True

testXCreateGroup7 ::Test
testXCreateGroup7 = testCase "XGROUP CREATE" $ do
    xgroupCreateOpts "somestream8" "somegroup" "0" XGroupCreateOpts {xGroupCreateMkStream    = True,
                                                                    xGroupCreateEntriesRead = Just "1234"} >>=? Ok
    return ()

testXRange ::Test
testXRange = testCase "xrange/xrevrange" $ do
    xadd "somestream8" "121" [("key1", "value1")]
    xadd "somestream8" "122" [("key2", "value2")]
    xadd "somestream8" "123" [("key3", "value3")]
    xadd "somestream8" "124" [("key4", "value4")]
    xrange "somestream8" "122" "123" Nothing >>=? [
        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]},
        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]}
        ]
    xrevRange "somestream8" "123" "122" Nothing >>=? [
        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]},
        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]}
        ]

testXpending ::Test
testXpending = testCase "xpending" $ do
    xadd "somestream8" "121" [("key1", "value1")]
    xadd "somestream8" "122" [("key2", "value2")]
    xadd "somestream8" "123" [("key3", "value3")]
    xadd "somestream8" "124" [("key4", "value4")]
    xgroupCreate "somestream8" "somegroup" "0"
    xreadGroup "somegroup" "consumer1" [("somestream8", ">")]
    xpendingSummary "somestream8" "somegroup" >>=? XPendingSummaryResponse {
        numPendingMessages = 4,
        smallestPendingMessageId = "121-0",
        largestPendingMessageId = "124-0",
        numPendingMessagesByconsumer = [("consumer1", 4)]
    }
    xpendingDetail "somestream8" "somegroup" "121" "121" 10 defaultXPendingDetailOpts >>@? (\case
            [XPendingDetailRecord{..}] -> do
                messageId HUnit.@=? "121-0"
            bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad
            )

testXpending7 ::Test
testXpending7 = testCase "xpending7" $ void $ runExceptT $ do
    ExceptT $ xadd "somestream8" "121" [("key1", "value1")]
    ExceptT $ xadd "somestream8" "122" [("key2", "value2")]
    ExceptT $ xadd "somestream8" "123" [("key3", "value3")]
    ExceptT $ xadd "somestream8" "124" [("key4", "value4")]
    ExceptT $ xgroupCreate "somestream8" "somegroup" "0"
    ExceptT $ xgroupCreate "somestream8" "somegroup2" "0"
    ExceptT $ xreadGroup "somegroup" "consumer1" [("somestream8", ">")]
    ExceptT $ xreadGroup "somegroup2" "consumer2" [("somestream8", ">")]
    ackedCount <- ExceptT $ xack "somestream8" "somegroup" ["121", "122", "123"]
    liftIO $ ackedCount HUnit.@=? 3
    pendingDetails <- ExceptT $ xpendingDetail "somestream8" "somegroup2" "123" "123" 10 XPendingDetailOpts
                    {xPendingDetailIdle     = Just 0,
                     xPendingDetailConsumer = Just "consumer2" }

    liftIO $ case pendingDetails of
        [XPendingDetailRecord{..}] -> do
            messageId HUnit.@=? "123-0"
        bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad

testXClaim ::Test
testXClaim =
  testCase "xclaim" $ void $ runExceptT $ do
    storedKey1 <- ExceptT $ xadd "somestream8" "121" [("key1", "value1")]
    liftIO $ storedKey1 HUnit.@=? "121-0"
    storedKey2 <- ExceptT $ xadd "somestream8" "122" [("key2", "value2")]
    liftIO $ storedKey2 HUnit.@=? "122-0"
    groupCreated <- ExceptT $ xgroupCreate "somestream8" "somegroup" "0"
    liftIO $ groupCreated HUnit.@=? Ok
    readResult <- ExceptT $ xreadGroupOpts
      "somegroup"
      "consumer1"
      [("somestream8", ">")]
      (defaultXReadGroupOpts {xReadGroupCount = Just 2})
    liftIO $ readResult HUnit.@=? Just
        [ XReadResponse
            { stream = "somestream8"
            , records =
                [ StreamsRecord
                    {recordId = "121-0", keyValues = [("key1", "value1")]}
                , StreamsRecord
                    {recordId = "122-0", keyValues = [("key2", "value2")]}
                ]
            }
        ]
    claimed <- ExceptT $ xclaim "somestream8" "somegroup" "consumer2" 0 defaultXClaimOpts ["121-0"]
    liftIO $ claimed HUnit.@=? [StreamsRecord {recordId = "121-0", keyValues = [("key1", "value1")]}]
    claimedJustIds <- ExceptT $ xclaimJustIds
      "somestream8"
      "somegroup"
      "consumer2"
      0
      defaultXClaimOpts
      ["122-0"]
    liftIO $ claimedJustIds HUnit.@=? ["122-0"]

testXAutoClaim7 ::Test
testXAutoClaim7 =
  testCase "xautoclaim" $ do
    xadd "somestream8" "121" [("key1", "value1")] >>=? "121-0"
    xadd "somestream8" "122" [("key2", "value2")] >>=? "122-0"
    xgroupCreate "somestream8" "somegroup" "0" >>=? Ok
    xreadGroupOpts "somegroup" "consumer1" [("somestream8", ">")] defaultXReadGroupOpts { xReadGroupCount = Just 2 }

    let opts = XAutoclaimOpts {
        xAutoclaimCount = Just 1
    }
    xautoclaimJustIdsOpts "somestream8" "somegroup" "consumer2" 0 "0-0" opts  >>@? (\case
        XAutoclaimResult{..} -> do
            xAutoclaimClaimedMessages HUnit.@=? ["121-0"]
            xAutoclaimDeletedMessages HUnit.@=? []
            return ())

    xtrim "somestream8" (trimOpts (TrimMaxlen 1) TrimExact) >>=? 1
    xautoclaim "somestream8" "somegroup" "consumer2" 0 "0-0" >>@? (\case
        XAutoclaimResult{..} -> do
            xAutoclaimClaimedMessages HUnit.@=? [StreamsRecord {
                recordId = "122-0",
                keyValues = [("key2", "value2")]
            }]
            xAutoclaimDeletedMessages HUnit.@=? ["121-0"]
            return ()
        )
    return ()

testXAckDel8 :: Test
testXAckDel8 = testCase "xackdel" $ do
    xadd "somestream8-1" "121" [("key1", "value1")] >>=? "121-0"
    xgroupCreate "somestream8-1" "somegroup1" "0" >>=? Ok
    xgroupCreate "somestream8-1" "somegroup2" "0" >>=? Ok
    xreadGroup "somegroup1" "consumer1" [("somestream8-1", ">")] >>@? const (pure ())
    xreadGroup "somegroup2" "consumer2" [("somestream8-1", ">")] >>@? const (pure ())

    let ackedOpts = defaultXEntryDeletionOpts { xEntryDeletionRefPolicy = XRefPolicyAcked }

    xackdelOpts "somestream8-1" "somegroup1" ("121-0" NE.:| []) ackedOpts
        >>=? [XEntryDeletionResultNotDeleted]
    xrange "somestream8-1" "-" "+" Nothing >>@? \records ->
        HUnit.assertEqual "entry should remain until all groups acknowledge it" 1 (length records)

    xackdelOpts "somestream8-1" "somegroup2" ("121-0" NE.:| []) ackedOpts
        >>=? [XEntryDeletionResultDeleted]
    xrange "somestream8-1" "-" "+" Nothing >>=? []

testXDelEx8 :: Test
testXDelEx8 = testCase "xdelex" $ do
    xadd "somestream8" "121" [("key1", "value1")] >>=? "121-0"
    xgroupCreate "somestream8" "somegroup1" "0" >>=? Ok

    let ackedOpts = defaultXEntryDeletionOpts { xEntryDeletionRefPolicy = XRefPolicyAcked }

    xdelexOpts "somestream8" ("121-0" NE.:| []) ackedOpts
        >>=? [XEntryDeletionResultNotDeleted]
    xrange "somestream8" "-" "+" Nothing >>@? \records ->
       HUnit.assertEqual "ACKED should not delete without consumer groups" 1 (length records)

    xgroupCreate "somestream8" "somegroup" "0" >>=? Ok
    xreadGroup "somegroup" "consumer1" [("somestream8", ">")] >>@? const (pure ())
    xdelex "somestream8" ("121-0" NE.:| [])
        >>=? [XEntryDeletionResultDeleted]
    xpendingSummary "somestream8" "somegroup" >>@? \summary ->
        numPendingMessages summary HUnit.@=? 1

testXInfo ::Test
-- This test does not work with pipelining because it relies on the certaino order of commands execution
-- and fails if commands reach different nodes.
testXInfo = testCase "xinfo" $ void $ runExceptT $ do
    _ <- ExceptT $ xadd "somestream8" "121" [("key1", "value1")]
    _ <- ExceptT $ xadd "somestream8" "122" [("key2", "value2")]
    _ <- ExceptT $ xgroupCreate "somestream8" "somegroup" "0"
    _ <- ExceptT $ xreadGroupOpts "somegroup" "consumer1" [("somestream8", ">")] defaultXReadGroupOpts { xReadGroupCount = Just 2 }

    z <- ExceptT $ xinfoConsumers "somestream8" "somegroup"
    liftIO $ case z of
        [XInfoConsumersResponse{..}] -> do
            xinfoConsumerName HUnit.@=? "consumer1"
            xinfoConsumerNumPendingMessages HUnit.@=? 2

        bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad

    x <- ExceptT $ xinfoGroups "somestream8"
    liftIO $ case x of
        [XInfoGroupsResponse{..}] -> do
            xinfoGroupsGroupName              HUnit.@=? "somegroup"
            xinfoGroupsNumConsumers           HUnit.@=? 1
            xinfoGroupsNumPendingMessages     HUnit.@=? 2
            xinfoGroupsLastDeliveredMessageId HUnit.@=? "122-0"

            (do xinfoGroupsEntriesRead          HUnit.@=? Nothing -- Redis 6
                xinfoGroupsLag                  HUnit.@=? Nothing) <|?>
                (do xinfoGroupsEntriesRead          HUnit.@=? Just 2 -- Redis 7
                    xinfoGroupsLag                  HUnit.@=? Just 0)

        bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad

    a <- ExceptT $ xinfoStream "somestream8"
    liftIO $ case a of
        XInfoStreamResponse{..} -> do
            xinfoStreamLength         HUnit.@=? 2
            xinfoStreamRadixTreeKeys  HUnit.@=? 1
            xinfoStreamRadixTreeNodes HUnit.@=? 2
            xinfoStreamNumGroups      HUnit.@=? 1
            xinfoStreamLastEntryId    HUnit.@=? "122-0"
            xinfoStreamFirstEntry     HUnit.@=? StreamsRecord {
                                                      recordId = "121-0"
                                                    , keyValues = [("key1", "value1")]}
            xinfoStreamLastEntry      HUnit.@=? StreamsRecord {
                                                      recordId = "122-0"
                                                    , keyValues = [("key2", "value2")] }
            (do xinfoMaxDeletedEntryId    HUnit.@=? Nothing -- Redis 6.0
                xinfoEntriesAdded         HUnit.@=? Nothing
                xinfoRecordedFirstEntryId HUnit.@=? Nothing) <|?> -- Redis 7.0
                (do xinfoMaxDeletedEntryId    HUnit.@=? Just "0-0"
                    xinfoEntriesAdded         HUnit.@=? Just 2
                    xinfoRecordedFirstEntryId HUnit.@=? Just "121-0")
        bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad
    return ()

testXDel ::Test
testXDel = testCase "xdel" $ do
    xadd "somestream8" "121" [("key1", "value1")]
    xadd "somestream8" "122" [("key2", "value2")]
    xdel "somestream8" ["122"] >>=? 1
    xlen "somestream8" >>=? 1

testVRange84 :: Test
testVRange84 = testCase "vrange" $ do
    vadd "word_embeddings" (0.1 NE.:| [1.2, 0.5]) "Redis" >>=? True
    vadd "word_embeddings" (0.2 NE.:| [1.1, 0.4]) "a7" >>=? True
    vadd "word_embeddings" (0.3 NE.:| [1.0, 0.3]) "b1" >>=? True
    vadd "word_embeddings" (0.4 NE.:| [0.9, 0.2]) "z9" >>=? True

    vrangeCount "word_embeddings" "[Redis" "+" 10 >>=? ["Redis", "a7", "b1", "z9"]
    vrangeCount "word_embeddings" "-" "+" 10 >>=? ["Redis", "a7", "b1", "z9"]
    vrangeCount "word_embeddings" "(a7" "+" 10 >>=? ["b1", "z9"]
    vrangeCount "word_embeddings" "-" "+" (-1) >>=? ["Redis", "a7", "b1", "z9"]

testRedis86Commands :: Test
testRedis86Commands = testCase "redis 8.6 commands" $ do
    xadd "idmp-stream" "*" [("field", "value")] >>@? const (pure ())

    xcfgset "idmp-stream" defaultXCfgSetOpts { xCfgSetIdmpDuration = Just 300 } >>= \case
        Left reply | isUnknownCommandReply reply -> pure ()
        Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected XCFGSET reply: " ++ show reply
        Right Ok -> do
            xcfgset "idmp-stream" defaultXCfgSetOpts
                { xCfgSetIdmpDuration = Just 600
                , xCfgSetIdmpMaxsize = Just 500
                }
                >>=? Ok
        Right status ->
            liftIO $ HUnit.assertFailure $ "Unexpected XCFGSET status: " ++ show status

    hotkeysStop >>= \case
        Left reply | isUnknownCommandReply reply || isHotkeysInactiveReply reply -> pure ()
        Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS STOP reply: " ++ show reply
        Right Ok -> pure ()
        Right status -> liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS STOP status: " ++ show status

    hotkeysReset >>= \case
        Left reply | isUnknownCommandReply reply -> pure ()
        Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS RESET reply: " ++ show reply
        Right Ok -> do
            hotkeysStartOpts
                (HotkeysMetricCPU NE.:| [HotkeysMetricNET])
                defaultHotkeysStartOpts { hotkeysStartTopKCount = Just 2 }
                >>= \case
                    Left reply | isUnknownCommandReply reply -> pure ()
                    Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS START reply: " ++ show reply
                    Right Ok -> do
                        set "hotkey:001" "payload" >>=? Ok
                        replicateM_ 25 $ do
                            incr "hotkey:counter" >>@? const (pure ())
                            get "hotkey:001" >>=? Just "payload"

                        hotkeysGet >>@? \HotkeysGetResponse{..} -> do
                            HUnit.assertBool "tracking should be active before HOTKEYS STOP" hotkeysGetTrackingActive
                            HUnit.assertBool "sample ratio should be positive" (hotkeysGetSampleRatio >= 1)
                            HUnit.assertBool "selected slots should not be empty" (not $ null hotkeysGetSelectedSlots)
                            HUnit.assertBool "collection duration should be non-negative" (hotkeysGetCollectionDurationMs >= 0)
                            HUnit.assertBool "expected CPU hotkeys to include generated keys" $
                                maybe False (any (\(key, _) -> "hotkey:" `Char8.isPrefixOf` key)) hotkeysGetByCpuTimeUs
                            HUnit.assertBool "expected NET hotkeys to include generated keys" $
                                maybe False (any (\(key, _) -> "hotkey:" `Char8.isPrefixOf` key)) hotkeysGetByNetBytes

                        hotkeysStop >>=? Ok
                        hotkeysGet >>@? \HotkeysGetResponse{..} ->
                            HUnit.assertBool "tracking should be stopped after HOTKEYS STOP" (not hotkeysGetTrackingActive)
                        hotkeysReset >>=? Ok
                    Right status ->
                        liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS START status: " ++ show status
        Right status ->
            liftIO $ HUnit.assertFailure $ "Unexpected HOTKEYS RESET status: " ++ show status

testRedis88Commands :: Test
testRedis88Commands = testCase "redis 8.8 commands" $ do
    increx "counter88" >>= \case
        Left reply | isUnknownCommandReply reply -> pure ()
        Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected INCREX reply: " ++ show reply
        Right (value, applied) -> do
            liftIO $ (1, 1) HUnit.@=? (value, applied)
            increxBy "counter88" 5 defaultIncrexOpts
                { increxLowerBound = Just 0
                , increxUpperBound = Just 10
                , increxExpiration = Just (IncrexSeconds 60)
                }
                >>=? (6, 5)
            ttl "counter88" >>@? \secondsLeft ->
                HUnit.assertBool "INCREX EX should set a TTL" (secondsLeft >= 0 && secondsLeft <= 60)
            increxByFloat "counter88:float" 0.5 defaultIncrexOpts
                { increxLowerBound = Just 0.0
                , increxUpperBound = Just 1.0
                }
                >>@? \(floatValue, floatApplied) ->
                    HUnit.assertBool "INCREX BYFLOAT should increment the floating-point value" $
                        abs (floatValue - 0.5) < 0.0001 && abs (floatApplied - 0.5) < 0.0001

            streamId <- xadd "stream88" "*" [("field", "value")] >>= \case
                Left reply -> liftIO (HUnit.assertFailure $ "Unexpected XADD reply: " ++ show reply) >> pure ""
                Right sid -> pure sid
            xidmprecord "stream88" "producer-1" "iid-1" streamId >>= \case
                Left reply | isUnknownCommandReply reply -> pure ()
                Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected XIDMPRECORD reply: " ++ show reply
                Right Ok -> pure ()
                Right status -> liftIO $ HUnit.assertFailure $ "Unexpected XIDMPRECORD status: " ++ show status

            xadd "stream88-nack" "1-0" [("field", "value")] >>=? "1-0"
            xgroupCreate "stream88-nack" "group88" "0" >>=? Ok
            xreadGroup "group88" "consumer88" [("stream88-nack", ">")] >>@? const (pure ())
            xnack "stream88-nack" "group88" XNackFail ("1-0" NE.:| []) >>=? 1

            arset "arr88" 0 ("alpha" NE.:| ["beta", "gamma"]) >>= \case
                Left reply | isUnknownCommandReply reply -> pure ()
                Left reply -> liftIO $ HUnit.assertFailure $ "Unexpected ARSET reply: " ++ show reply
                Right createdSlots -> do
                    liftIO $ 3 HUnit.@=? createdSlots
                    arcount "arr88" >>=? 3
                    arlen "arr88" >>=? 3
                    armget "arr88" (0 NE.:| [2, 3]) >>=? [Just "alpha", Just "gamma", Nothing]
                    argetrange "arr88" 0 3 >>=? [Just "alpha", Just "beta", Just "gamma", Nothing]

                    argrep "arr88" "-" "+" (ARGrepExact "beta" NE.:| []) >>=? [1]
                    argrepWithValuesOpts "arr88" "-" "+" (ARGrepMatch "a" NE.:| []) defaultARGrepOpts
                        { arGrepLimit = Just 2
                        }
                        >>=? ARIndexValuePairsResponse [(0, "alpha"), (1, "beta")]

                    arinfo "arr88" >>@? \ARInfoResponse{..} -> do
                        3 HUnit.@=? arInfoCount
                        3 HUnit.@=? arInfoLength
                        HUnit.assertBool "ARINFO should report a positive slice size" (arInfoSliceSize > 0)

                    arseek "arr88" 5 >>=? True
                    arinsert "arr88" ("delta" NE.:| ["epsilon"]) >>=? 6
                    arnext "arr88" >>=? Just 7
                    arlastitems "arr88" 2 >>=? [Just "delta", Just "epsilon"]
                    arlastitemsOpts "arr88" 2 defaultARLastItemsOpts { arLastItemsReverse = True } >>=? [Just "epsilon", Just "delta"]
                    arscanOpts "arr88" 0 10 defaultARScanOpts { arScanLimit = Just 3 } >>=? ARIndexValuePairsResponse [(0, "alpha"), (1, "beta"), (2, "gamma")]
                    ardel "arr88" (1 NE.:| [5]) >>=? 2
                    arcount "arr88" >>=? 3

                    arset "nums88" 0 ("1" NE.:| ["2", "3"]) >>=? 3
                    aropValue "nums88" 0 2 AROpSum >>=? Just "6"
                    aropCount "nums88" 0 2 AROpUsed >>=? Just 3

                    arring "ring88" 3 ("v0" NE.:| ["v1", "v2", "v3"]) >>=? 0
                    arcount "ring88" >>=? 3
                    arlastitems "ring88" 3 >>=? [Just "v1", Just "v2", Just "v3"]

testVectorSet8 :: Test
testVectorSet8 = testCase "vector sets" $ do
    let key = "word_embeddings"
        members = ["apple", "apples", "pear", "pears", "potato"]
        insert element attrs vector =
            vaddOpts key vector element defaultVAddOpts
                { vAddQuantization = Just VAddNoQuant
                , vAddAttributes = attrs
                }

    insert "apple" (Just "{\"len\":5,\"kind\":\"fruit\"}") (1.0 NE.:| [0.0, 0.0]) >>=? True
    insert "apples" (Just "{\"len\":6,\"kind\":\"fruit\"}") (0.9 NE.:| [0.1, 0.0]) >>=? True
    insert "pear" (Just "{\"len\":4,\"kind\":\"fruit\"}") (0.8 NE.:| [0.2, 0.0]) >>=? True
    insert "pears" Nothing (0.75 NE.:| [0.25, 0.05]) >>=? True
    insert "potato" (Just "{\"len\":6,\"kind\":\"vegetable\"}") (0.0 NE.:| [1.0, 0.0]) >>=? True

    vcard key >>=? 5
    vdim key >>=? 3
    vismember key "apple" >>=? True
    vismember key "orange" >>=? False

    vemb key "apple" >>@? \case
        [x, y, z] -> do
            HUnit.assertBool "VEMB should approximately reconstruct the inserted vector" $
                abs (x - 1.0) < 0.001 && abs y < 0.001 && abs z < 0.001
        vector ->
            HUnit.assertFailure $ "Unexpected VEMB response: " ++ show vector

    vembRaw key "apple" >>@? \case
        Just VEmbRawResponse{..} -> do
            VQuantizationFP32 HUnit.@=? vEmbRawQuantization
            HUnit.assertBool "raw vector blob should not be empty" (BS.length vEmbRawData > 0)
            HUnit.assertBool "vector norm should be positive" (vEmbRawNorm > 0)
            Nothing HUnit.@=? vEmbRawRange
        reply ->
            HUnit.assertFailure $ "Unexpected VEMB RAW response: " ++ show reply

    vgetattr key "apple" >>=? Just "{\"len\":5,\"kind\":\"fruit\"}"
    vsetattr key "pears" "{\"len\":5,\"kind\":\"fruit\"}" >>=? True
    vgetattr key "pears" >>=? Just "{\"len\":5,\"kind\":\"fruit\"}"
    vsetattr key "pears" "" >>=? True
    vgetattr key "pears" >>=? Nothing

    vinfo key >>@? \case
        Just VInfoResponse{..} -> do
            HUnit.assertBool "quantization should be reported as f32/fp32" $
                vInfoQuantization == Just "f32" || vInfoQuantization == Just "fp32"
            Just 3 HUnit.@=? vInfoVectorDim
            Just 5 HUnit.@=? vInfoSize
            HUnit.assertBool "max level should be reported" $
                maybe False (>= 0) vInfoMaxLevel
        reply ->
            HUnit.assertFailure $ "Unexpected VINFO response: " ++ show reply

    vlinks key "apple" >>@? \case
        Just (VLinksResponse layers) ->
            HUnit.assertBool "VLINKS should return at least one adjacent element" $
                any (not . null) layers
        reply ->
            HUnit.assertFailure $ "Unexpected VLINKS response: " ++ show reply

    vlinksWithScores key "apple" >>@? \case
        Just (VLinksWithScoresResponse layers) -> do
            HUnit.assertBool "VLINKS WITHSCORES should return at least one adjacent element" $
                any (not . null) layers
            HUnit.assertBool "VLINKS WITHSCORES should only return known members" $
                all (\(neighbor, _) -> neighbor `elem` members)
                    [ pair | layer <- layers, pair <- layer ]
        reply ->
            HUnit.assertFailure $ "Unexpected VLINKS WITHSCORES response: " ++ show reply

    vrandmember key >>@? \member ->
        HUnit.assertBool "VRANDMEMBER should return one of the inserted elements" $
            maybe False (`elem` members) member

    vrandmemberCount key 3 >>@? \randomMembers -> do
        HUnit.assertEqual "VRANDMEMBER count" 3 (length randomMembers)
        HUnit.assertBool "VRANDMEMBER count should only return known members" $
            all (`elem` members) randomMembers

    vrange key "-" "+" >>=? members
    vrangeCount key "[apple" "[pear" 10 >>=? ["apple", "apples", "pear"]

    vsim key (VSimByElement "apple") >>@? \similar -> do
        HUnit.assertBool "VSIM should return at least one match" (not $ null similar)
        case similar of
            firstMatch:_ ->
                HUnit.assertEqual "VSIM first match" "apple" firstMatch
            [] ->
                HUnit.assertFailure "VSIM returned no matches"

    vsimOpts key (VSimByValues (1.0 NE.:| [0.0, 0.0])) defaultVSimOpts { vSimCount = Just 2 } >>@? \similar -> do
        HUnit.assertEqual "VSIM VALUES count" 2 (length similar)
        case similar of
            firstMatch:_ ->
                HUnit.assertEqual "VSIM VALUES first match" "apple" firstMatch
            [] ->
                HUnit.assertFailure "VSIM VALUES returned no matches"

    vsimWithScoresOpts key (VSimByElement "apple") defaultVSimOpts { vSimCount = Just 3 } >>@? \similar -> do
        HUnit.assertEqual "VSIM WITHSCORES count" 3 (length similar)
        case similar of
            (firstMatch, firstScore):_ -> do
                HUnit.assertEqual "VSIM WITHSCORES first match" "apple" firstMatch
                HUnit.assertBool "VSIM WITHSCORES self similarity should be close to 1" $
                    firstScore > 0.99
            [] ->
                HUnit.assertFailure "VSIM WITHSCORES returned no matches"

    vsimWithScoresWithAttribsOpts key (VSimByElement "apple") defaultVSimOpts { vSimCount = Just 3 } >>@? \VSimWithAttribsResponse{..} -> do
        HUnit.assertEqual "VSIM WITHATTRIBS count" 3 (length vSimWithAttribsResults)
        case vSimWithAttribsResults of
            firstMatch:_ -> do
                HUnit.assertEqual "VSIM WITHATTRIBS first match" "apple" (vSimResultElement firstMatch)
                Just "{\"len\":5,\"kind\":\"fruit\"}" HUnit.@=? vSimResultAttributes firstMatch
            [] ->
                HUnit.assertFailure "VSIM WITHATTRIBS returned no matches"

    vrem key "potato" >>=? True
    vismember key "potato" >>=? False
    vcard key >>=? 4

testClusterSlotStats8 :: Test
testClusterSlotStats8 = testCase "cluster slot-stats" $ do
    clusterSlotStatsSlotsRange 0 16383 >>@? \ClusterSlotStatsResponse{..} -> do
        HUnit.assertBool "CLUSTER SLOT-STATS SLOTSRANGE should return at least one slot" $
            not (null clusterSlotStatsResponseEntries)
        forM_ clusterSlotStatsResponseEntries $ \ClusterSlotStatsResponseEntry{..} -> do
            HUnit.assertBool "slot number should be in the valid cluster range" $
                clusterSlotStatsResponseEntrySlot >= 0 && clusterSlotStatsResponseEntrySlot <= 16383
            HUnit.assertBool "key-count should be present" $
                maybe False (>= 0) clusterSlotStatsResponseEntryKeyCount

    clusterSlotStatsOrderByOpts ClusterSlotStatsKeyCount
        defaultClusterSlotStatsOrderByOpts { clusterSlotStatsOrderByLimit = Just 1 }
        >>@? \ClusterSlotStatsResponse{..} ->
            HUnit.assertBool "ORDERBY with LIMIT should return at most one entry" $
                length clusterSlotStatsResponseEntries <= 1

testClusterMigration84 :: Test
testClusterMigration84 = testCase "cluster migration" $ do
    clusterMigrationCancelAll >>@? \cancelled ->
        HUnit.assertBool "cancel count should be non-negative" (cancelled >= 0)

    clusterMigrationStatusAll >>@? \ClusterMigrationStatusResponse{..} ->
        forM_ clusterMigrationStatusTasks $ \ClusterMigrationTask{..} -> do
            HUnit.assertBool "migration task id should not be empty" (clusterMigrationTaskId /= "")
            HUnit.assertBool "migration task retries should be non-negative when present" $
                maybe True (>= 0) clusterMigrationTaskRetries

testXTrim ::Test
testXTrim = testCase "xtrim" $ do
    xadd "somestream8" "121" [("key1", "value1")]
    xadd "somestream8" "122" [("key2", "value2")]
    xadd "somestream8" "123" [("key3", "value3")]
    streamId <- fromRight "" <$> xadd "somestream8" "124" [("key4", "value4")]
    xadd "somestream8" "125" [("key5", "value5")]
    xtrim "somestream8" (trimOpts (TrimMaxlen 3) TrimExact) >>=? 2
    xtrim "somestream8" (trimOpts (TrimMinId streamId) TrimExact) >>=? 1