# SPDX-FileCopyrightText: 2024 Redict Contributors
# SPDX-FileCopyrightText: 2024 Salvatore Sanfilippo <antirez at gmail dot com>
#
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-License-Identifier: LGPL-3.0-only

set testmodule [file normalize tests/modules/hooks.so]

tags "modules" {
    start_server [list overrides [list loadmodule "$testmodule" appendonly yes]] {
        test {Test module aof save on server start from empty} {
            assert {[r hooks.event_count persistence-syncaof-start] == 1}
        }

        test {Test clients connection / disconnection hooks} {
            for {set j 0} {$j < 2} {incr j} {
                set rd1 [redict_deferring_client]
                $rd1 close
            }
            assert {[r hooks.event_count client-connected] > 1}
            assert {[r hooks.event_count client-disconnected] > 1}
        }

        test {Test module client change event for blocked client} {
            set rd [redict_deferring_client]
            # select db other than 0
            $rd select 1
            # block on key
            $rd brpop foo 0
            # kill blocked client
            r client kill skipme yes
            # assert server is still up
            assert_equal [r ping] PONG
            $rd close
        } 

        test {Test module cron hook} {
            after 100
            assert {[r hooks.event_count cron-loop] > 0}
            set hz [r hooks.event_last cron-loop]
            assert_equal $hz 10
        }

        test {Test module loaded / unloaded hooks} {
            set othermodule [file normalize tests/modules/infotest.so]
            r module load $othermodule
            r module unload infotest
            assert_equal [r hooks.event_last module-loaded] "infotest"
            assert_equal [r hooks.event_last module-unloaded] "infotest"
        }

        test {Test module aofrw hook} {
            r debug populate 1000 foo 10000 ;# 10mb worth of data
            r config set rdbcompression no ;# rdb progress is only checked once in 2mb
            r BGREWRITEAOF
            waitForBgrewriteaof r
            assert_equal [string match {*module-event-persistence-aof-start*} [exec tail -20 < [srv 0 stdout]]] 1
            assert_equal [string match {*module-event-persistence-end*} [exec tail -20 < [srv 0 stdout]]] 1
        }

        test {Test module aof load and rdb/aof progress hooks} {
            # create some aof tail (progress is checked only once in 1000 commands)
            for {set j 0} {$j < 4000} {incr j} {
                r set "bar$j" x
            }
            # set some configs that will cause many loading progress events during aof loading
            r config set key-load-delay 500
            r config set dynamic-hz no
            r config set hz 500
            r DEBUG LOADAOF
            assert_equal [r hooks.event_last loading-aof-start] 0
            assert_equal [r hooks.event_last loading-end] 0
            assert {[r hooks.event_count loading-rdb-start] == 0}
            assert_lessthan 2 [r hooks.event_count loading-progress-rdb] ;# comes from the preamble section
            assert_lessthan 2 [r hooks.event_count loading-progress-aof]
            if {$::verbose} {
                puts "rdb progress events [r hooks.event_count loading-progress-rdb]"
                puts "aof progress events [r hooks.event_count loading-progress-aof]"
            }
        }
        # undo configs before next test
        r config set dynamic-hz yes
        r config set key-load-delay 0

        test {Test module rdb save hook} {
            # debug reload does: save, flush, load:
            assert {[r hooks.event_count persistence-syncrdb-start] == 0}
            assert {[r hooks.event_count loading-rdb-start] == 0}
            r debug reload
            assert {[r hooks.event_count persistence-syncrdb-start] == 1}
            assert {[r hooks.event_count loading-rdb-start] == 1}
        }

        test {Test key unlink hook} {
            r set testkey1 hello
            r del testkey1
            assert {[r hooks.event_count key-info-testkey1] == 1}
            assert_equal [r hooks.event_last key-info-testkey1] testkey1
            r lpush testkey1 hello
            r lpop testkey1
            assert {[r hooks.event_count key-info-testkey1] == 2}
            assert_equal [r hooks.event_last key-info-testkey1] testkey1
            r set testkey2 world
            r unlink testkey2
            assert {[r hooks.event_count key-info-testkey2] == 1}
            assert_equal [r hooks.event_last key-info-testkey2] testkey2
        }

        test {Test removed key event} {
            r set str abcd
            r set str abcde
            # For String Type value is returned
            assert_equal {abcd overwritten} [r hooks.is_key_removed str]
            assert_equal -1 [r hooks.pexpireat str]

            r del str
            assert_equal {abcde deleted} [r hooks.is_key_removed str]
            assert_equal -1 [r hooks.pexpireat str]

            # test int encoded string
            r set intstr 12345678
            # incr doesn't fire event
            r incr intstr
            catch {[r hooks.is_key_removed intstr]} output
            assert_match {ERR * removed} $output
            r del intstr
            assert_equal {12345679 deleted} [r hooks.is_key_removed intstr]

            catch {[r hooks.is_key_removed not-exists]} output
            assert_match {ERR * removed} $output

            r hset hash f v
            r hdel hash f
            assert_equal {0 deleted} [r hooks.is_key_removed hash]

            r hset hash f v a b
            r del hash
            assert_equal {2 deleted} [r hooks.is_key_removed hash]

            r lpush list 1
            r lpop list
            assert_equal {0 deleted} [r hooks.is_key_removed list]

            r lpush list 1 2 3
            r del list
            assert_equal {3 deleted} [r hooks.is_key_removed list]

            r sadd set 1
            r spop set
            assert_equal {0 deleted} [r hooks.is_key_removed set]

            r sadd set 1 2 3 4
            r del set
            assert_equal {4 deleted} [r hooks.is_key_removed set]

            r zadd zset 1 f
            r zpopmin zset
            assert_equal {0 deleted} [r hooks.is_key_removed zset]

            r zadd zset 1 f 2 d
            r del zset
            assert_equal {2 deleted} [r hooks.is_key_removed zset]

            r xadd stream 1-1 f v
            r xdel stream 1-1
            # Stream does not delete object when del entry
            catch {[r hooks.is_key_removed stream]} output
            assert_match {ERR * removed} $output
            r del stream
            assert_equal {0 deleted} [r hooks.is_key_removed stream]

            r xadd stream 2-1 f v
            r del stream
            assert_equal {1 deleted} [r hooks.is_key_removed stream]

            # delete key because of active expire
            set size [r dbsize]
            r set active-expire abcd px 1
            #ensure active expire
            wait_for_condition 50 100 {
                [r dbsize] == $size
            } else {
                fail "Active expire not trigger"
            }
            assert_equal {abcd expired} [r hooks.is_key_removed active-expire]
            # current time is greater than pexpireat
            set now [r time]
            set mill [expr ([lindex $now 0]*1000)+([lindex $now 1]/1000)]
            assert {$mill >= [r hooks.pexpireat active-expire]}

            # delete key because of lazy expire
            r debug set-active-expire 0
            r set lazy-expire abcd px 1
            after 10
            r get lazy-expire
            assert_equal {abcd expired} [r hooks.is_key_removed lazy-expire]
            set now [r time]
            set mill [expr ([lindex $now 0]*1000)+([lindex $now 1]/1000)]
            assert {$mill >= [r hooks.pexpireat lazy-expire]}
            r debug set-active-expire 1

            # delete key not yet expired
            set now [r time]
            set expireat [expr ([lindex $now 0]*1000)+([lindex $now 1]/1000)+1000000]
            r set not-expire abcd pxat $expireat
            r del not-expire
            assert_equal {abcd deleted} [r hooks.is_key_removed not-expire]
            assert_equal $expireat [r hooks.pexpireat not-expire]

            # Test key evict
            set used [expr {[s used_memory] - [s mem_not_counted_for_evict]}]
            set limit [expr {$used+100*1024}]
            set old_policy [lindex [r config get maxmemory-policy] 1]
            r config set maxmemory $limit
            # We set policy volatile-random, so only keys with ttl will be evicted
            r config set maxmemory-policy volatile-random
            r setex volatile-key 10000 x
            # We use SETBIT here, so we can set a big key and get the used_memory
            # bigger than maxmemory. Next command will evict volatile keys. We
            # can't use SET, as SET uses big input buffer, so it will fail.
            r setbit big-key 1600000 0 ;# this will consume 200kb
            r getbit big-key 0
            assert_equal {x evicted} [r hooks.is_key_removed volatile-key]
            r config set maxmemory-policy $old_policy
            r config set maxmemory 0
        } {OK} {needs:debug}

        test {Test flushdb hooks} {
            r flushdb
            assert_equal [r hooks.event_last flush-start] 9
            assert_equal [r hooks.event_last flush-end] 9
            r flushall
            assert_equal [r hooks.event_last flush-start] -1
            assert_equal [r hooks.event_last flush-end] -1
        }

        # replication related tests
        set master [srv 0 client]
        set master_host [srv 0 host]
        set master_port [srv 0 port]
        start_server {} {
            r module load $testmodule
            set replica [srv 0 client]
            set replica_host [srv 0 host]
            set replica_port [srv 0 port]
            $replica replicaof $master_host $master_port

            wait_replica_online $master

            test {Test master link up hook} {
                assert_equal [r hooks.event_count masterlink-up] 1
                assert_equal [r hooks.event_count masterlink-down] 0
            }

            test {Test role-replica hook} {
                assert_equal [r hooks.event_count role-replica] 1
                assert_equal [r hooks.event_count role-master] 0
                assert_equal [r hooks.event_last role-replica] [s 0 master_host]
            }

            test {Test replica-online hook} {
                assert_equal [r -1 hooks.event_count replica-online] 1
                assert_equal [r -1 hooks.event_count replica-offline] 0
            }

            test {Test master link down hook} {
                r client kill type master
                assert_equal [r hooks.event_count masterlink-down] 1

                wait_for_condition 50 100 {
                    [string match {*master_link_status:up*} [r info replication]]
                } else {
                    fail "Replica didn't reconnect"
                }

                assert_equal [r hooks.event_count masterlink-down] 1
                assert_equal [r hooks.event_count masterlink-up] 2
            }

            wait_for_condition 50 10 {
                [string match {*master_link_status:up*} [r info replication]]
            } else {
                fail "Can't turn the instance into a replica"
            }

            $replica replicaof no one

            test {Test role-master hook} {
                assert_equal [r hooks.event_count role-replica] 1
                assert_equal [r hooks.event_count role-master] 1
                assert_equal [r hooks.event_last role-master] {}
            }

            test {Test replica-offline hook} {
                assert_equal [r -1 hooks.event_count replica-online] 2
                assert_equal [r -1 hooks.event_count replica-offline] 2
            }
            # get the replica stdout, to be used by the next test
            set replica_stdout [srv 0 stdout]
        }

        test {Test swapdb hooks} {
            r swapdb 0 10
            assert_equal [r hooks.event_last swapdb-first] 0
            assert_equal [r hooks.event_last swapdb-second] 10
        }

        test {Test configchange hooks} {
            r config set rdbcompression no 
            assert_equal [r hooks.event_last config-change-count] 1
            assert_equal [r hooks.event_last config-change-first] rdbcompression
        }

        # look into the log file of the server that just exited
        test {Test shutdown hook} {
            assert_equal [string match {*module-event-shutdown*} [exec tail -5 < $replica_stdout]] 1
        }
    }

    start_server {} {
        test {OnLoad failure will handle un-registration} {
            catch {r module load $testmodule noload}
            r flushall
            r ping
        }
    }
}
