start_server {tags {"pubsub network"}} {
    if {$::singledb} {
        set db 0
    } else {
        set db 9
    }

    foreach resp {2 3} {
        set rd1 [redis_deferring_client]
        if {[lsearch $::denytags "resp3"] >= 0} {
            if {$resp == 3} {continue}
        } elseif {$::force_resp3} {
            if {$resp == 2} {continue}
        }

        $rd1 hello $resp
        $rd1 read

        test "Pub/Sub PING on RESP$resp" {
            subscribe $rd1 somechannel
            # While subscribed to non-zero channels PING works in Pub/Sub mode.
            $rd1 ping
            $rd1 ping "foo"
            # In RESP3, the SUBSCRIBEd client can issue any command and get a reply, so the PINGs are standard
            # In RESP2, only a handful of commands are allowed after a client is SUBSCRIBED (PING is one of them).
            # For some reason, the reply in that case is an array with two elements: "pong"  and argv[1] or an empty string
            # God knows why. Done in commit 2264b981
            if {$resp == 3} {
                assert_equal {PONG} [$rd1 read]
                assert_equal {foo} [$rd1 read]
            } else {
                assert_equal {pong {}} [$rd1 read]
                assert_equal {pong foo} [$rd1 read]
            }
            unsubscribe $rd1 somechannel
            # Now we are unsubscribed, PING should just return PONG.
            $rd1 ping
            assert_equal {PONG} [$rd1 read]

        }
        $rd1 close
    }

    test "PUBLISH/SUBSCRIBE basics" {
        set rd1 [redis_deferring_client]

        # subscribe to two channels
        assert_equal {1 2} [subscribe $rd1 {chan1 chan2}]
        assert_equal 1 [r publish chan1 hello]
        assert_equal 1 [r publish chan2 world]
        assert_equal {message chan1 hello} [$rd1 read]
        assert_equal {message chan2 world} [$rd1 read]

        # unsubscribe from one of the channels
        unsubscribe $rd1 {chan1}
        assert_equal 0 [r publish chan1 hello]
        assert_equal 1 [r publish chan2 world]
        assert_equal {message chan2 world} [$rd1 read]

        # unsubscribe from the remaining channel
        unsubscribe $rd1 {chan2}
        assert_equal 0 [r publish chan1 hello]
        assert_equal 0 [r publish chan2 world]

        # clean up clients
        $rd1 close
    }

    test "PUBLISH/SUBSCRIBE with two clients" {
        set rd1 [redis_deferring_client]
        set rd2 [redis_deferring_client]

        assert_equal {1} [subscribe $rd1 {chan1}]
        assert_equal {1} [subscribe $rd2 {chan1}]
        assert_equal 2 [r publish chan1 hello]
        assert_equal {message chan1 hello} [$rd1 read]
        assert_equal {message chan1 hello} [$rd2 read]

        # clean up clients
        $rd1 close
        $rd2 close
    }

    test "PUBLISH/SUBSCRIBE after UNSUBSCRIBE without arguments" {
        set rd1 [redis_deferring_client]
        assert_equal {1 2 3} [subscribe $rd1 {chan1 chan2 chan3}]
        unsubscribe $rd1
        wait_for_condition 100 10 {
            [regexp {cmd=unsubscribe} [r client list]] eq 1
        } else {
            fail "unsubscribe did not arrive"
        }
        assert_equal 0 [r publish chan1 hello]
        assert_equal 0 [r publish chan2 hello]
        assert_equal 0 [r publish chan3 hello]

        # clean up clients
        $rd1 close
    }

    test "SUBSCRIBE to one channel more than once" {
        set rd1 [redis_deferring_client]
        assert_equal {1 1 1} [subscribe $rd1 {chan1 chan1 chan1}]
        assert_equal 1 [r publish chan1 hello]
        assert_equal {message chan1 hello} [$rd1 read]

        # clean up clients
        $rd1 close
    }

    test "UNSUBSCRIBE from non-subscribed channels" {
        set rd1 [redis_deferring_client]
        assert_equal {0 0 0} [unsubscribe $rd1 {foo bar quux}]

        # clean up clients
        $rd1 close
    }

    test "PUBLISH/PSUBSCRIBE basics" {
        set rd1 [redis_deferring_client]

        # subscribe to two patterns
        assert_equal {1 2} [psubscribe $rd1 {foo.* bar.*}]
        assert_equal 1 [r publish foo.1 hello]
        assert_equal 1 [r publish bar.1 hello]
        assert_equal 0 [r publish foo1 hello]
        assert_equal 0 [r publish barfoo.1 hello]
        assert_equal 0 [r publish qux.1 hello]
        assert_equal {pmessage foo.* foo.1 hello} [$rd1 read]
        assert_equal {pmessage bar.* bar.1 hello} [$rd1 read]

        # unsubscribe from one of the patterns
        assert_equal {1} [punsubscribe $rd1 {foo.*}]
        assert_equal 0 [r publish foo.1 hello]
        assert_equal 1 [r publish bar.1 hello]
        assert_equal {pmessage bar.* bar.1 hello} [$rd1 read]

        # unsubscribe from the remaining pattern
        assert_equal {0} [punsubscribe $rd1 {bar.*}]
        assert_equal 0 [r publish foo.1 hello]
        assert_equal 0 [r publish bar.1 hello]

        # clean up clients
        $rd1 close
    }

    test "PUBLISH/PSUBSCRIBE with two clients" {
        set rd1 [redis_deferring_client]
        set rd2 [redis_deferring_client]

        assert_equal {1} [psubscribe $rd1 {chan.*}]
        assert_equal {1} [psubscribe $rd2 {chan.*}]
        assert_equal 2 [r publish chan.foo hello]
        assert_equal {pmessage chan.* chan.foo hello} [$rd1 read]
        assert_equal {pmessage chan.* chan.foo hello} [$rd2 read]

        # clean up clients
        $rd1 close
        $rd2 close
    }

    test "PUBLISH/PSUBSCRIBE after PUNSUBSCRIBE without arguments" {
        set rd1 [redis_deferring_client]
        assert_equal {1 2 3} [psubscribe $rd1 {chan1.* chan2.* chan3.*}]
        punsubscribe $rd1
        wait_for_condition 100 10 {
            [regexp {cmd=punsubscribe} [r client list]] eq 1
        } else {
            fail "punsubscribe did not arrive"
        }
        assert_equal 0 [r publish chan1.hi hello]
        assert_equal 0 [r publish chan2.hi hello]
        assert_equal 0 [r publish chan3.hi hello]

        # clean up clients
        $rd1 close
    }

    test "PubSub messages with CLIENT REPLY OFF" {
        set rd [redis_deferring_client]
        $rd hello 3
        $rd read ;# Discard the hello reply

        # Test that the subscribe/psubscribe notification is ok
        $rd client reply off
        assert_equal {1} [subscribe $rd channel]
        assert_equal {2} [psubscribe $rd ch*]

        # Test that the publish notification is ok
        $rd client reply off
        assert_equal 2 [r publish channel hello]
        assert_equal {message channel hello} [$rd read]
        assert_equal {pmessage ch* channel hello} [$rd read]

        # Test that the unsubscribe/punsubscribe notification is ok
        $rd client reply off
        assert_equal {1} [unsubscribe $rd channel]
        assert_equal {0} [punsubscribe $rd ch*]

        $rd close
    } {0} {resp3}

    test "PUNSUBSCRIBE from non-subscribed channels" {
        set rd1 [redis_deferring_client]
        assert_equal {0 0 0} [punsubscribe $rd1 {foo.* bar.* quux.*}]

        # clean up clients
        $rd1 close
    }

    test "NUMSUB returns numbers, not strings (#1561)" {
        r pubsub numsub abc def
    } {abc 0 def 0}

    test "NUMPATs returns the number of unique patterns" {
        set rd1 [redis_deferring_client]
        set rd2 [redis_deferring_client]

        # Three unique patterns and one that overlaps
        psubscribe $rd1 "foo*"
        psubscribe $rd2 "foo*"
        psubscribe $rd1 "bar*"
        psubscribe $rd2 "baz*"

        set patterns [r pubsub numpat]

        # clean up clients
        punsubscribe $rd1
        punsubscribe $rd2
        assert_equal 3 $patterns
        $rd1 close
        $rd2 close
    }

    test "Mix SUBSCRIBE and PSUBSCRIBE" {
        set rd1 [redis_deferring_client]
        assert_equal {1} [subscribe $rd1 {foo.bar}]
        assert_equal {2} [psubscribe $rd1 {foo.*}]

        assert_equal 2 [r publish foo.bar hello]
        assert_equal {message foo.bar hello} [$rd1 read]
        assert_equal {pmessage foo.* foo.bar hello} [$rd1 read]

        # clean up clients
        $rd1 close
    }

    test "PUNSUBSCRIBE and UNSUBSCRIBE should always reply" {
        # Make sure we are not subscribed to any channel at all.
        r punsubscribe
        r unsubscribe
        # Now check if the commands still reply correctly.
        set reply1 [r punsubscribe]
        set reply2 [r unsubscribe]
        concat $reply1 $reply2
    } {punsubscribe {} 0 unsubscribe {} 0}

    ### Keyspace events notification tests

    test "Keyspace notifications: we receive keyspace notifications" {
        r config set notify-keyspace-events KA
        set rd1 [redis_deferring_client]
        $rd1 CLIENT REPLY OFF ;# Make sure it works even if replies are silenced
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        assert_equal "pmessage * __keyspace@${db}__:foo set" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: we receive keyevent notifications" {
        r config set notify-keyspace-events EA
        r del foo
        set rd1 [redis_deferring_client]
        $rd1 CLIENT REPLY SKIP ;# Make sure it works even if replies are silenced
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        assert_equal "pmessage * __keyevent@${db}__:set foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: we can receive both kind of events" {
        r config set notify-keyspace-events KEA
        r del foo
        set rd1 [redis_deferring_client]
        $rd1 CLIENT REPLY ON ;# Just coverage
        assert_equal {OK} [$rd1 read]
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        assert_equal "pmessage * __keyspace@${db}__:foo set" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:set foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: we are able to mask events" {
        r config set notify-keyspace-events KEl
        r del mylist
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        r lpush mylist a
        # No notification for set, because only list commands are enabled.
        assert_equal "pmessage * __keyspace@${db}__:mylist lpush" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:lpush mylist" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: general events test" {
        r config set notify-keyspace-events KEg
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        r expire foo 1
        r del foo
        assert_equal "pmessage * __keyspace@${db}__:foo expire" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:expire foo" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:foo del" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:del foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: list events test" {
        r config set notify-keyspace-events KEl
        r del mylist
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r lpush mylist a
        r rpush mylist a
        r rpop mylist
        assert_equal "pmessage * __keyspace@${db}__:mylist lpush" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:lpush mylist" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mylist rpush" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:rpush mylist" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mylist rpop" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:rpop mylist" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: set events test" {
        r config set notify-keyspace-events Ks
        r del myset
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r sadd myset a b c d
        r srem myset x
        r sadd myset x y z
        r srem myset x
        assert_equal "pmessage * __keyspace@${db}__:myset sadd" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myset sadd" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myset srem" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: zset events test" {
        r config set notify-keyspace-events Kz
        r del myzset
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r zadd myzset 1 a 2 b
        r zrem myzset x
        r zadd myzset 3 x 4 y 5 z
        r zrem myzset x
        assert_equal "pmessage * __keyspace@${db}__:myzset zadd" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myzset zadd" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myzset zrem" [$rd1 read]
        $rd1 close
    }

    foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
    test "Keyspace notifications: hash events test ($type)" {
        r config set hash-max-listpack-entries $max_lp_entries
        r config set notify-keyspace-events Khg
        r del myhash
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r hmset myhash yes 1 no 0 f1 1 f2 2 f3_hdel 3
        r hincrby myhash yes 10
        r hexpire myhash 999999 FIELDS 1 yes
        r hexpireat myhash [expr {[clock seconds] + 999999}] NX FIELDS 1 no
        r hpexpire myhash 999999 FIELDS 1 yes
        r hpersist myhash FIELDS 1 yes
        r hpexpire myhash 0 FIELDS 1 yes
        assert_encoding $type myhash
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hincrby" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        # Test that we will get `hexpired` notification when
        # a hash field is removed by active expire.
        r hpexpire myhash 10 FIELDS 1 no
        after 100 ;# Wait for active expire
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]

        # Test that when a field with TTL is deleted by commands like hdel without
        # updating the global DS, active expire will not send a notification.
        r hpexpire myhash 100 FIELDS 1 f3_hdel
        r hdel myhash f3_hdel
        after 200 ;# Wait for active expire
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        # Test that we will get `hexpired` notification when
        # a hash field is removed by lazy expire.
        r debug set-active-expire 0
        r hpexpire myhash 10 FIELDS 2 f1 f2
        after 20
        r hmget myhash f1 f2 ;# Trigger lazy expire
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        # We should get only one `hexpired` notification even two fields was expired.
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        # We should get a `del` notification after all fields were expired.
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
        r debug set-active-expire 1


        # Test HSETEX, HGETEX and HGETDEL notifications
        r hsetex myhash FIELDS 3 f4 v4 f5 v5 f6 v6
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]

        # hgetex sets ttl in past
        r hgetex myhash PX 0 FIELDS 1 f4
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        # hgetex sets ttl
        r hgetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f5
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]

        # hgetex persists field
        r hgetex myhash PERSIST FIELDS 1 f5
        assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read]

        # hgetex sets expiry for one field and lazy expiry deletes another field
        # (KSN should be 1-hexpired 2-hexpire)
        r debug set-active-expire 0
        r hsetex myhash PX 1 FIELDS 1 f5 v5
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 10
        r hgetex myhash EX 100 FIELDS 2 f5 f6
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]

        # hgetex lazy expiry deletes the only field and the key
        # (KSN should be 1-hexpired 2-del)
        r hsetex myhash PX 1 FIELDS 2 f5 v5 f6 v6
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 10
        r hgetex myhash FIELDS 2 f5 f6
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
        r debug set-active-expire 1

        # hgetex sets an expired ttl for the only field and deletes the key
        # (KSN should be 1-hdel 2-del)
        r hsetex myhash EX 100 FIELDS 1 f5 v5
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 10
        r hgetex myhash PX 0 FIELDS 1 f5
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        r hsetex myhash FIELDS 2 f5 v5 f6 v6
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]

        # hgetdel deletes a field
        r hgetdel myhash FIELDS 1 f5
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        # hsetex sets field and expiry time
        r hsetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f6 v6
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]

        # hsetex sets field and ttl in the past
        r hsetex myhash PX 0 FIELDS 1 f6 v6
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        # Test that we will get `hexpired` notification when a hash field is
        # removed by lazy expire using hgetdel command
        r debug set-active-expire 0
        r hsetex myhash PX 10 FIELDS 1 f1 v1
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]

        # Set another field
        r hsetex myhash FIELDS 1 f2 v2
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        # Wait until field expires
        after 20
        r hgetdel myhash FIELDS 1 f1
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        # Get and delete the only field
        r hgetdel myhash FIELDS 1 f2
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        # HGETDEL deletes one field and the other field is lazily expired
        # (KSN should be 1-hexpired 2-hdel)
        r hsetex myhash FIELDS 2 f1 v1 f2 v2
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        r hsetex myhash PX 1 FIELDS 1 f3 v3
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 10
        r hgetdel myhash FIELDS 2 f1 f3
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        # HGETDEL, deletes one field and the last field lazily expires
        # (KSN should be 1-hexpired 2-hdel 3-del)
        r hsetex myhash FIELDS 1 f1 v1
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        r hsetex myhash PX 1 FIELDS 1 f2 v2
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 10
        r hgetdel myhash FIELDS 2 f1 f2
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
        r debug set-active-expire 1

        $rd1 close
    } {0} {needs:debug}
    } ;# foreach

    test "Keyspace notifications: stream events test" {
        r config set notify-keyspace-events Kt
        r del mystream
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r xgroup create mystream mygroup $ mkstream
        r xgroup createconsumer mystream mygroup Bob
        set id [r xadd mystream 1 field1 A]
        r xreadgroup group mygroup Alice STREAMS mystream >
        r xclaim mystream mygroup Mike 0 $id force
        # Not notify because of "Lee" not exists.
        r xgroup delconsumer mystream mygroup Lee
        # Not notify because of "Bob" exists.
        r xautoclaim mystream mygroup Bob 0 $id
        r xgroup delconsumer mystream mygroup Bob
        assert_equal "pmessage * __keyspace@${db}__:mystream xgroup-create" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mystream xgroup-createconsumer" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mystream xadd" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mystream xgroup-createconsumer" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mystream xgroup-createconsumer" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:mystream xgroup-delconsumer" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications:FXX/FNX with HSETEX cmd" {
        r config set notify-keyspace-events Khxg
        r del myhash
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r debug set-active-expire 0

        # FXX on logically expired field
        r hset myhash f v
        r hset myhash f2 v
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        r hpexpire myhash 10 FIELDS 1 f
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 15
        assert_equal [r HSETEX myhash FXX PX 10 FIELDS 1 f v] 0
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        r hdel myhash f2
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal 0 [r exists myhash]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        # FXX with past expiry
        r HSET myhash f1 v1
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        set past [expr {[clock seconds] - 2}]
        assert_equal [r hsetex myhash FXX EXAT $past FIELDS 1 f1 v1] 1
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        # FXX overwrite + full key expiry
        r hset myhash f v
        r hset myhash f2 v
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        r hpexpire myhash 10 FIELDS 1 f
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 15
        set past [expr {[clock milliseconds] - 5000}]
        assert_equal [r hsetex myhash FXX PXAT $past FIELDS 1 f v] 0
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        r hpexpire myhash 10 FIELDS 1 f2
        after 15
        r hget myhash f2
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]

        # FNX on logically expired field
        r del myhash
        r hset myhash f v
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        r hpexpire myhash 10 FIELDS 1 f
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
        after 15
        assert_equal [r HSETEX myhash FNX PX 1000 FIELDS 1 f v] 1
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]

        # FNX with past expiry
        r del myhash
        r hset myhash f v
        assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        set past [expr {[clock seconds] - 2}]
        assert_equal [r hsetex myhash FNX EXAT $past FIELDS 1 f1 v1] 1
        # f1 is created and immediately expired
        assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
        assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]

        r debug set-active-expire 1
        $rd1 close
    } {0} {needs:debug}

    test "Keyspace notifications: expired events (triggered expire)" {
        r config set notify-keyspace-events Ex
        r del foo
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r psetex foo 100 1
        wait_for_condition 50 100 {
            [r exists foo] == 0
        } else {
            fail "Key does not expire?!"
        }
        assert_equal "pmessage * __keyevent@${db}__:expired foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: expired events (background expire)" {
        r config set notify-keyspace-events Ex
        r del foo
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r psetex foo 100 1
        assert_equal "pmessage * __keyevent@${db}__:expired foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: evicted events" {
        r config set notify-keyspace-events Ee
        r config set maxmemory-policy allkeys-lru
        r flushdb
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        r config set maxmemory 1
        assert_equal "pmessage * __keyevent@${db}__:evicted foo" [$rd1 read]
        r config set maxmemory 0
        $rd1 close
        r config set maxmemory-policy noeviction
    } {OK} {needs:config-maxmemory}

    test "Keyspace notifications: test CONFIG GET/SET of event flags" {
        r config set notify-keyspace-events gKE
        assert_equal {gKE} [lindex [r config get notify-keyspace-events] 1]
        r config set notify-keyspace-events {$lshzxeKE}
        assert_equal {$lshzxeKE} [lindex [r config get notify-keyspace-events] 1]
        r config set notify-keyspace-events KA
        assert_equal {AK} [lindex [r config get notify-keyspace-events] 1]
        r config set notify-keyspace-events EA
        assert_equal {AE} [lindex [r config get notify-keyspace-events] 1]
    }

    test "Keyspace notifications: new key test" {
        r config set notify-keyspace-events En
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]
        r set foo bar
        # second set of foo should not cause a 'new' event
        r set foo baz 
        r set bar bar
        assert_equal "pmessage * __keyevent@${db}__:new foo" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:new bar" [$rd1 read]
        $rd1 close
    }

    ### overwritten and type_changed events

    test "Keyspace notifications: overwritten events - string to string" {
        r config set notify-keyspace-events Eo
        r del foo
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # First set - should not trigger overwritten (new key)
        r set foo bar

        # Second set - should trigger overwritten (same type)
        r set foo baz

        assert_equal "pmessage * __keyevent@${db}__:overwritten foo" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: type_changed events - hash to string" {
        r config set notify-keyspace-events Ec
        r del testkey
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # Set as hash first
        r hset testkey field "hash_value"

        # Change to string - should trigger type_changed
        r set testkey "string_value"

        assert_equal "pmessage * __keyevent@${db}__:type_changed testkey" [$rd1 read]
        $rd1 close
    }

    test "Keyspace notifications: both overwritten and type_changed events" {
        r config set notify-keyspace-events Eoc
        r del testkey3
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # Set as hash first
        r hset testkey3 field "hash_value"

        # Change to string - should trigger both overwritten and type_changed
        r set testkey3 "string_value"

        assert_equal "pmessage * __keyevent@${db}__:overwritten testkey3" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed testkey3" [$rd1 read]

        $rd1 close
    }

    test "Keyspace notifications: configuration flags work correctly" {
        # Test that 'o' flag enables override notifications
        r config set notify-keyspace-events o
        set config [r config get notify-keyspace-events]
        assert {[lindex $config 1] eq "o"}

        # Test that 'c' flag enables type_changed notifications
        r config set notify-keyspace-events c
        set config [r config get notify-keyspace-events]
        assert {[lindex $config 1] eq "c"}

        # Test that both flags can be combined
        r config set notify-keyspace-events oc
        set config [r config get notify-keyspace-events]
        assert {[lindex $config 1] eq "oc"}
    }

    ### RESTORE command tests for type_changed KSN types

    test "Keyspace notifications: RESTORE REPLACE different type - restore, overwritten and type_changed events" {
        r config set notify-keyspace-events Egoc
        r del restore_test_key3

        # Create a string value and dump it (do this before subscribing)
        r set temp_key "string_value"
        set dump_data [r dump temp_key]
        r del temp_key

        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # Create initial hash key
        r hset restore_test_key3 field "hash_value"

        # Restore with REPLACE - should emit restore, overwritten and type_changed events
        r restore restore_test_key3 0 $dump_data REPLACE

        assert_equal "pmessage * __keyevent@${db}__:restore restore_test_key3" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:overwritten restore_test_key3" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed restore_test_key3" [$rd1 read]

        $rd1 close
    }

    ### SET command tests for overwritten and type_changed KSN types

    test "Keyspace notifications: SET on existing string key - overwritten event" {
        r config set notify-keyspace-events EAo
        r del set_test_key1
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # Create initial string key
        r set set_test_key1 "initial_value"
        assert_equal "pmessage * __keyevent@${db}__:set set_test_key1" [$rd1 read]

        # Set new value on existing string key - should emit overwritten event
        r set set_test_key1 "new_value"

        assert_equal "pmessage * __keyevent@${db}__:overwritten set_test_key1" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:set set_test_key1" [$rd1 read]

        $rd1 close
    }

    test "Keyspace notifications: setKey on existing different type key - overwritten and type_changed events" {
        r config set notify-keyspace-events Eoc

        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        r flushdb
        r hset set_test_key2 field "hash_value"
        r set set_test_key2 "string_value"
        assert_equal "pmessage * __keyevent@${db}__:overwritten set_test_key2" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed set_test_key2" [$rd1 read]

        # overwritten and type_changed events should be emitted for any->any
        # type conversion that uses the setKey command
        r flushdb
        r lpush l{t} 1 2 3
        r sadd s1{t} "A"
        r sadd s2{t} "B"
        r sunionstore l{t} s1{t} s2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten l{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed l{t}" [$rd1 read]

        r flushdb
        r sadd s1{t} "A"
        r set x{t} "\x0f"
        r set y{t} "\xff"
        r bitop and s1{t} x{t} y{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten s1{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed s1{t}" [$rd1 read]

        $rd1 close
    }

    test "Keyspace notifications: overwritten and type_changed events for RENAME and COPY commands" {
        r config set notify-keyspace-events Eoc

        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        # test COPY events
        r flushdb
        r hset hs{t} 1 2 3 4
        r lpush l{t} 1 2 3 4
        r copy hs{t} l{t} replace

        assert_equal "pmessage * __keyevent@${db}__:overwritten l{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed l{t}" [$rd1 read]

        # test rename RENAME events
        r flushdb
        r hset hs{t} field "hash_value"
        r sadd x{t} 1 2 3
        r rename x{t} hs{t}

        assert_equal "pmessage * __keyevent@${db}__:overwritten hs{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed hs{t}" [$rd1 read]

        $rd1 close
    }

    test "Keyspace notifications: overwritten and type_changed for *STORE* commands" {
        r config set notify-keyspace-events Eoc

        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 *]

        r flushdb
        r set x{t} x

        # SORT
        r lpush l{t} 4 3 2 1
        r sort l{t} store x{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten x{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed x{t}" [$rd1 read]

        # SDIFFSTORE
        r sadd s1{t} a b c d
        r sadd s2{t} b e f
        r sdiffstore x{t} s1{t} s2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten x{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed x{t}" [$rd1 read]

        # SINTERSTORE
        r set d1{t} x
        r sinterstore d1{t} s1{t} s2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d1{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d1{t}" [$rd1 read]

        # SUNIONSTORE
        r set d2{t} x
        r sunionstore d2{t} s1{t} s2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d2{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d2{t}" [$rd1 read]

        # ZUNIONSTORE
        r set d3{t} x
        r zadd z1{t} 1 a 2 b
        r zadd z2{t} 3 c 4 d
        r zunionstore d3{t} 2 z1{t} z2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d3{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d3{t}" [$rd1 read]

        # ZINTERSTORE
        r set d4{t} x
        r zadd z2{t} 2 a
        r zinterstore d4{t} 2 z1{t} z2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d4{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d4{t}" [$rd1 read]

        # ZDIFFSTORE
        r set d5{t} x
        r zdiffstore d5{t} 2 z1{t} z2{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d5{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d5{t}" [$rd1 read]

        # ZRANGESTORE
        r set d6{t} x
        r zadd zsrc{t} 1 a 2 b 3 c 4 d
        r zrangestore d6{t} zsrc{t} 1 2
        assert_equal "pmessage * __keyevent@${db}__:overwritten d6{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d6{t}" [$rd1 read]

        # GEORADIUS with STORE
        r set d7{t} x
        r geoadd geo{t} 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
        r georadius geo{t} 15 37 200 km store d7{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d7{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d7{t}" [$rd1 read]

        # GEORADIUS with STOREDIST
        r set d8{t} x
        r georadius geo{t} 15 37 200 km storedist d8{t}
        assert_equal "pmessage * __keyevent@${db}__:overwritten d8{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d8{t}" [$rd1 read]

        # GEOSEARCHSTORE
        r set d9{t} x
        r geosearchstore d9{t} geo{t} fromlonlat 15 37 byradius 200 km
        assert_equal "pmessage * __keyevent@${db}__:overwritten d9{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d9{t}" [$rd1 read]

        # GEOSEARCHSTORE with STOREDIST
        r set d10{t} x
        r geosearchstore d10{t} geo{t} fromlonlat 15 37 byradius 200 km storedist
        assert_equal "pmessage * __keyevent@${db}__:overwritten d10{t}" [$rd1 read]
        assert_equal "pmessage * __keyevent@${db}__:type_changed d10{t}" [$rd1 read]

        $rd1 close
    }

    ### Subkey-level notification tests for HASH type ###

    # Helper: build expected payload "event|len:field0,len:field1,..."
    proc build_expected_payload {event prefix count} {
        set parts {}
        for {set i 0} {$i < $count} {incr i} {
            set f "${prefix}${i}"
            lappend parts "[string length $f]:$f"
        }
        return "${event}|[join $parts ,]"
    }

    # Compare subkey notification payloads as sets (order-insensitive).
    # Parses "event|f1,f2,..." and checks event matches and fields match as sets.
    proc assert_subkey_payload_equal {expected actual} {
        set ep [split $expected "|"]
        set ap [split $actual "|"]
        assert_equal [lindex $ep 0] [lindex $ap 0] ;# event name
        set ef [lsort [split [lindex $ep 1] ","]]
        set af [lsort [split [lindex $ap 1] ","]]
        assert_equal $ef $af
    }

    # Generate N field-value pairs: {f0 v0 f1 v1 ...}
    proc gen_field_values {prefix n} {
        set args {}
        for {set i 0} {$i < $n} {incr i} {
            lappend args "${prefix}${i}" "v${i}"
        }
        return $args
    }

    # Generate N field names: {f0 f1 ...}
    proc gen_fields {prefix n} {
        set fields {}
        for {set i 0} {$i < $n} {incr i} {
            lappend fields "${prefix}${i}"
        }
        return $fields
    }

    # Subkey notification: subkeyspace channel
    foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
        r config set hash-max-listpack-entries $max_lp_entries
        r config set notify-keyspace-events Sh
        set rd1 [redis_deferring_client]
        assert_equal {1} [subscribe $rd1 "__subkeyspace@${db}__:myhash"]

    test "Subkey notifications: subkeyspace - HSET single field ($type)" {
        r del myhash
        r hset myhash f1 v1
        assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1" [$rd1 read]
    }

    test "Subkey notifications: subkeyspace - HINCRBY ($type)" {
        r del myhash
        r hset myhash counter 10
        r hincrby myhash counter 5
        assert_equal "message __subkeyspace@${db}__:myhash hset|7:counter" [$rd1 read]
        assert_equal "message __subkeyspace@${db}__:myhash hincrby|7:counter" [$rd1 read]
    }

    test "Subkey notifications: subkeyspace - HSETNX ($type)" {
        r del myhash
        r hsetnx myhash newfield val
        assert_equal "message __subkeyspace@${db}__:myhash hset|8:newfield" [$rd1 read]
    }

    test "Subkey notifications: subkeyspace - HINCRBYFLOAT ($type)" {
        r del myhash
        r hset myhash counter 10.5
        r hincrbyfloat myhash counter 2.5
        assert_equal "message __subkeyspace@${db}__:myhash hset|7:counter" [$rd1 read]
        assert_equal "message __subkeyspace@${db}__:myhash hincrbyfloat|7:counter" [$rd1 read]
    }

    # Test with N=3 (stack path, within FIELDS_STACK_SIZE=16) and
    # N=32 (heap path, exceeds FIELDS_STACK_SIZE).
    foreach N {3 32} {

    test "Subkey notifications: HSET $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        set expected [build_expected_payload "hset" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HDEL $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset notification
        r hdel myhash {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hdel" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HGETDEL $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset notification
        r hgetdel myhash FIELDS $N {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hdel" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HEXPIRE $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset notification
        r hexpire myhash 1000 FIELDS $N {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hexpire" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HEXPIRE past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset notification
        r hexpireat myhash 1 FIELDS $N {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hdel" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HPERSIST $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        set fields [gen_fields "f" $N]
        r hset myhash {*}[gen_field_values "f" $N]
        r hexpire myhash 1000 FIELDS $N {*}$fields
        $rd1 read ;# consume hset
        $rd1 read ;# consume hexpire
        r hpersist myhash FIELDS $N {*}$fields
        set expected [build_expected_payload "hpersist" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HGETEX with expire $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset
        r hgetex myhash EX 1000 FIELDS $N {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hexpire" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HGETEX with persist $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        set fields [gen_fields "f" $N]
        r hset myhash {*}[gen_field_values "f" $N]
        r hexpire myhash 1000 FIELDS $N {*}$fields
        $rd1 read ;# consume hset
        $rd1 read ;# consume hexpire
        r hgetex myhash PERSIST FIELDS $N {*}$fields
        set expected [build_expected_payload "hpersist" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HGETEX past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hset myhash {*}[gen_field_values "f" $N]
        $rd1 read ;# consume hset
        r hgetex myhash PX 0 FIELDS $N {*}[gen_fields "f" $N]
        set expected [build_expected_payload "hdel" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
    }

    test "Subkey notifications: HSETEX $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hsetex myhash EX 1000 FIELDS $N {*}[gen_field_values "f" $N]
        set expected_hset [build_expected_payload "hset" "f" $N]
        set expected_hexpire [build_expected_payload "hexpire" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected_hset" [$rd1 read]
        assert_equal "message __subkeyspace@${db}__:myhash $expected_hexpire" [$rd1 read]
    }

    test "Subkey notifications: HSETEX past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        r hsetex myhash PX 0 FIELDS $N {*}[gen_field_values "f" $N]
        set expected_hset [build_expected_payload "hset" "f" $N]
        set expected_hdel [build_expected_payload "hdel" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected_hset" [$rd1 read]
        assert_equal "message __subkeyspace@${db}__:myhash $expected_hdel" [$rd1 read]
    }

    test "Subkey notifications: lazy field expiry triggers hexpired $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        # Create N+1 fields, expire N of them; keep one to prevent hash deletion.
        set fields [gen_fields "f" $N]
        set args [gen_field_values "f" $N]
        lappend args "keep" "val"
        r hset myhash {*}$args
        r debug set-active-expire 0
        r hpexpire myhash 10 FIELDS $N {*}$fields
        $rd1 read ;# consume hset
        $rd1 read ;# consume hexpire
        # Trigger lazy expiry by reading the fields
        after 100
        r hmget myhash {*}$fields
        set expected_hexpired [build_expected_payload "hexpired" "f" $N]
        assert_equal "message __subkeyspace@${db}__:myhash $expected_hexpired" [$rd1 read]
        r debug set-active-expire 1
    } {OK} {needs:debug}

    test "Subkey notifications: active field expiry triggers hexpired $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
        r del myhash
        # Create N+1 fields, expire N of them; keep one to prevent hash deletion.
        set fields [gen_fields "f" $N]
        set args [gen_field_values "f" $N]
        lappend args "keep" "val"
        r hset myhash {*}$args
        r hpexpire myhash 10 FIELDS $N {*}$fields
        $rd1 read ;# consume hset
        $rd1 read ;# consume hexpire
        # Wait for active expiry; field order depends on hash table iteration,
        # so compare as set.
        set expected_hexpired [build_expected_payload "hexpired" "f" $N]
        set actual [$rd1 read]
        set prefix "message __subkeyspace@${db}__:myhash "
        assert_equal $prefix [string range $actual 0 [expr {[string length $prefix]-1}]]
        assert_subkey_payload_equal $expected_hexpired [string range $actual [string length $prefix] end]
    }
    } ;# end foreach N
    $rd1 close
    } ;# end foreach type

    # Subkey notification format tests for subkeyevent/subkeyspaceitem/subkeyspaceevent
    # Full command coverage is done via subkeyspace channel below; here we only verify channel format.
    foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
        r config set hash-max-listpack-entries $max_lp_entries

    test "Subkey notifications: subkeyevent format ($type)" {
        r config set notify-keyspace-events Th
        r del myhash
        set rd1 [redis_deferring_client]
        assert_equal {1} [subscribe $rd1 "__subkeyevent@${db}__:hset"]
        r hset myhash f1 v1 f2 v2 f3 v3
        assert_equal "message __subkeyevent@${db}__:hset 6:myhash|2:f1,2:f2,2:f3" [$rd1 read]
        $rd1 close
    }

    test "Subkey notifications: subkeyspaceitem format ($type)" {
        r config set notify-keyspace-events Ih
        r del myhash
        set rd1 [redis_deferring_client]
        $rd1 subscribe "__subkeyspaceitem@${db}__:myhash\nf1"
        $rd1 read ;# consume subscribe confirmation
        r hset myhash f1 v1
        set msg [$rd1 read]
        assert_equal "message" [lindex $msg 0]
        assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg 1]
        assert_equal "hset" [lindex $msg 2]
        $rd1 close
    }

    test "Subkey notifications: subkeyspaceitem per-subkey delivery with psubscribe ($type)" {
        r config set notify-keyspace-events Ih
        r del myhash
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 "__subkeyspaceitem@${db}__:myhash*"]
        r hset myhash f1 v1 f2 v2
        # Should get one notification per subkey
        set msg1 [$rd1 read]
        set msg2 [$rd1 read]
        assert_equal "pmessage" [lindex $msg1 0]
        assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg1 2]
        assert_equal "hset" [lindex $msg1 3]
        assert_equal "pmessage" [lindex $msg2 0]
        assert_equal "__subkeyspaceitem@${db}__:myhash\nf2" [lindex $msg2 2]
        assert_equal "hset" [lindex $msg2 3]
        $rd1 close
    }

    test "Subkey notifications: subkeyspaceitem skips key with newline ($type)" {
        r config set notify-keyspace-events Ih
        r del "key\nwith\nnewline"
        set rd1 [redis_deferring_client]
        assert_equal {1} [psubscribe $rd1 "__subkeyspaceitem@${db}__:*"]
        r hset "key\nwith\nnewline" f1 v1
        # Normal key to verify notifications still work
        r hset normalkey f1 v1
        # Should only get notification for normalkey
        set msg [$rd1 read]
        assert_equal "pmessage" [lindex $msg 0]
        assert_equal "__subkeyspaceitem@${db}__:normalkey\nf1" [lindex $msg 2]
        assert_equal "hset" [lindex $msg 3]
        r del "key\nwith\nnewline"
        r del normalkey
        $rd1 close
    }

    test "Subkey notifications: subkeyspaceevent format ($type)" {
        r config set notify-keyspace-events Vh
        r del myhash
        set rd1 [redis_deferring_client]
        assert_equal {1} [subscribe $rd1 "__subkeyspaceevent@${db}__:hset|myhash"]
        r hset myhash f1 v1 f2 v2
        assert_equal "message __subkeyspaceevent@${db}__:hset|myhash 2:f1,2:f2" [$rd1 read]
        $rd1 close
    }
    } ;

    # Test all 4 channels enabled simultaneously
    test "Subkey notifications: all 4 channels enabled simultaneously" {
        r config set notify-keyspace-events STIVh
        r del myhash
        set rd_s [redis_deferring_client]
        set rd_t [redis_deferring_client]
        set rd_i [redis_deferring_client]
        set rd_v [redis_deferring_client]
        assert_equal {1} [subscribe $rd_s "__subkeyspace@${db}__:myhash"]
        assert_equal {1} [subscribe $rd_t "__subkeyevent@${db}__:hset"]
        assert_equal {1} [subscribe $rd_v "__subkeyspaceevent@${db}__:hset|myhash"]
        $rd_i subscribe "__subkeyspaceitem@${db}__:myhash\nf1"
        $rd_i read ;# consume subscribe confirmation
        r hset myhash f1 v1
        assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1" [$rd_s read]
        assert_equal "message __subkeyevent@${db}__:hset 6:myhash|2:f1" [$rd_t read]
        assert_equal "message __subkeyspaceevent@${db}__:hset|myhash 2:f1" [$rd_v read]
        set msg_i [$rd_i read]
        assert_equal "message" [lindex $msg_i 0]
        assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg_i 1]
        assert_equal "hset" [lindex $msg_i 2]
        $rd_s close
        $rd_t close
        $rd_i close
        $rd_v close
    }

    # Test that subkey notifications are triggered on replica after replication
    test "Subkey notifications: replica receives subkey notifications after replication" {
        start_server {tags {"repl external:skip"}} {
            set master [srv -1 client]
            set master_host [srv -1 host]
            set master_port [srv -1 port]
            set replica [srv 0 client]

            $replica replicaof $master_host $master_port
            wait_for_sync $replica

            # Enable subkeyspace notifications on replica
            $replica config set notify-keyspace-events Sh

            # Subscribe on replica
            set rd1 [redis_deferring_client -1]
            assert_equal {1} [subscribe $rd1 "__subkeyspace@${db}__:myhash"]

            # Write on master
            $master hset myhash f1 v1 f2 v2
            $master hpexpire myhash 100 FIELDS 2 f1 f2

            # Replica should receive subkey notification
            assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1,2:f2" [$rd1 read]
            assert_equal "message __subkeyspace@${db}__:myhash hexpire|2:f1,2:f2" [$rd1 read]
            assert_equal "message __subkeyspace@${db}__:myhash hexpired|2:f1,2:f2" [$rd1 read]
            $rd1 close
            $master del myhash
        }
    }

    test "publish to self inside multi" {
        r hello 3
        r subscribe foo
        r multi
        r ping abc
        r publish foo bar
        r publish foo vaz
        r ping def
        assert_equal [r exec] {abc 1 1 def}
        assert_equal [r read] {message foo bar}
        assert_equal [r read] {message foo vaz}
    } {} {resp3}

    test "publish to self inside script" {
        r hello 3
        r subscribe foo
        set res [r eval {
                redis.call("ping","abc")
                redis.call("publish","foo","bar")
                redis.call("publish","foo","vaz")
                redis.call("ping","def")
                return "bla"} 0]
        assert_equal $res {bla}
        assert_equal [r read] {message foo bar}
        assert_equal [r read] {message foo vaz}
    } {} {resp3}

    test "unsubscribe inside multi, and publish to self" {
        r hello 3

        # Note: SUBSCRIBE and UNSUBSCRIBE with multiple channels in the same command,
        # breaks the multi response, see https://github.com/redis/redis/issues/12207
        # this is just a temporary sanity test to detect unintended breakage.

        # subscribe for 3 channels actually emits 3 "responses"
        assert_equal "subscribe foo 1" [r subscribe foo bar baz]
        assert_equal "subscribe bar 2" [r read]
        assert_equal "subscribe baz 3" [r read]

        r multi
        r ping abc
        r unsubscribe bar
        r unsubscribe baz
        r ping def
        assert_equal [r exec] {abc {unsubscribe bar 2} {unsubscribe baz 1} def}

        # published message comes after the publish command's response.
        assert_equal [r publish foo vaz] {1}
        assert_equal [r read] {message foo vaz}
    } {} {resp3}
}

start_server {tags {"pubsub network"}} {
    # Helper proc for tests that subscribe multiple times until hitting OOM
    proc test_subscribe_oom_loop {cmd description clients} {
        test "$cmd $description fails with OOM when memory limit exceeded" {
            # Set 10MB memory limit
            r config set maxmemory 10485760
            r config set maxmemory-policy noeviction
            
            # Create clients
            if {$clients == 1} {
                set rd [redis_deferring_client]
            } else {
                set rd1 [redis_deferring_client]
                set rd2 [redis_deferring_client]
            }
            
            set base_str [string repeat "a" 2048]
            set success_count 0
            set oom_occurred 0
            
            # Try to subscribe until we hit OOM
            for {set i 0} {$i < 5000} {incr i} {
                # Select client
                if {$clients == 1} {
                    set client $rd
                } else {
                    set client [expr {$i % 2 ? $rd1 : $rd2}]
                }
                
                # Build channel/pattern name
                if {$cmd eq "psubscribe"} {
                    set channel_name "${base_str}${i}*"
                } else {
                    set channel_name "${base_str}${i}"
                }
                
                $client $cmd $channel_name
                if {[catch {$client read} err]} {
                    if {[string match "*OOM command not allowed*" $err]} {
                        set oom_occurred 1
                        break
                    }
                    error "Unexpected error: $err"
                }
                incr success_count
            }
            
            # Verify we had at least one success and hit OOM
            assert {$success_count > 10}
            assert {$oom_occurred == 1}
            
            # Close clients
            if {$clients == 1} {
                $rd close
            } else {
                $rd1 close
                $rd2 close
            }
        }
    }

    # Helper proc for tests with single large channel that immediately fails
    proc test_subscribe_large_channel_oom {cmd channel_type} {
        test "$cmd with large $channel_type name fails due to OOM" {
            # Set maxmemory to 2MB
            r config set maxmemory 2097152
            r config set maxmemory-policy noeviction
            
            # Create large channel/pattern name: 2MB
            set channel_name [string repeat "a" 2097152]
            
            # Create a single pubsub client
            set rd [redis_deferring_client]
            
            # Subscribe should fail with OOM error
            $rd $cmd $channel_name
            assert_error "*OOM command not allowed when used memory > 'maxmemory'*" {$rd read}
            
            # Cleanup
            $rd close
        }
    }

    # Helper proc for tests with small success then large failure
    proc test_subscribe_small_then_large_oom {cmd channel_type} {
        test "$cmd succeeds with small $channel_type but fails with large $channel_type due to OOM" {
            # Set maxmemory to 5MB
            r config set maxmemory 5242880
            r config set maxmemory-policy noeviction
            
            # Create channel names: first 10KB, second 5MB
            set channel1 [string repeat "a" 10240]
            set channel2 [string repeat "b" 5242880]
            
            # Create a single pubsub client
            set rd [redis_deferring_client]
            
            # First subscribe should succeed (10KB)
            $rd $cmd $channel1
            set reply1 [$rd read]
            assert_equal [list $cmd] [lindex $reply1 0]
            
            # Second subscribe should fail with OOM error (5MB exceeds limit)
            $rd $cmd $channel2
            assert_error "*OOM command not allowed when used memory > 'maxmemory'*" {$rd read}
            
            # Cleanup
            $rd close
        }
    }

    # Multiple subscriptions until OOM tests
    test_subscribe_oom_loop "subscribe" "" 1
    test_subscribe_oom_loop "ssubscribe" "" 1
    test_subscribe_oom_loop "psubscribe" "" 1
    test_subscribe_oom_loop "subscribe" "with 2 clients" 2
    test_subscribe_oom_loop "ssubscribe" "with 2 clients" 2
    test_subscribe_oom_loop "psubscribe" "with 2 clients" 2

    # Single large channel immediate OOM tests
    test_subscribe_large_channel_oom "subscribe" "channel"
    test_subscribe_large_channel_oom "psubscribe" "pattern"
    test_subscribe_large_channel_oom "ssubscribe" "shard channel"

    # Small success then large failure tests
    test_subscribe_small_then_large_oom "subscribe" "channel"
    test_subscribe_small_then_large_oom "psubscribe" "pattern"
    test_subscribe_small_then_large_oom "ssubscribe" "channel"
}
