# 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/propagate.so]
set miscmodule [file normalize tests/modules/misc.so]
set keyspace_events [file normalize tests/modules/keyspace_events.so]

tags "modules" {
    test {Modules can propagate in async and threaded contexts} {
        start_server [list overrides [list loadmodule "$testmodule"]] {
            set replica [srv 0 client]
            set replica_host [srv 0 host]
            set replica_port [srv 0 port]
            $replica module load $keyspace_events
            start_server [list overrides [list loadmodule "$testmodule"]] {
                set master [srv 0 client]
                set master_host [srv 0 host]
                set master_port [srv 0 port]
                $master module load $keyspace_events

                # Start the replication process...
                $replica replicaof $master_host $master_port
                wait_for_sync $replica
                after 1000

                test {module propagates from timer} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.timer

                    wait_for_condition 500 10 {
                        [$replica get timer] eq "3"
                    } else {
                        fail "The two counters don't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {select *}
                        {incr timer}
                        {incr timer}
                        {incr timer}
                    }
                    close_replication_stream $repl
                }

                test {module propagation with notifications} {
                    set repl [attach_to_replication_stream]

                    $master set x y

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {set x y}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagation with notifications with multi} {
                    set repl [attach_to_replication_stream]

                    $master multi
                    $master set x1 y1
                    $master set x2 y2
                    $master exec

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {set x1 y1}
                        {incr notifications}
                        {set x2 y2}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagation with notifications with active-expire} {
                    $master debug set-active-expire 1
                    set repl [attach_to_replication_stream]

                    $master set asdf1 1 PX 300
                    $master set asdf2 2 PX 300
                    $master set asdf3 3 PX 300

                    wait_for_condition 500 10 {
                        [$replica keys asdf*] eq {}
                    } else {
                        fail "Not all keys have expired"
                    }

                    # Note whenever there's double notification: SET with PX issues two separate
                    # notifications: one for "set" and one for "expire"
                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {incr notifications}
                        {set asdf1 1 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {set asdf2 2 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {set asdf3 3 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {incr testkeyspace:expired}
                        {del asdf*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {incr testkeyspace:expired}
                        {del asdf*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {incr testkeyspace:expired}
                        {del asdf*}
                        {exec}
                    }
                    close_replication_stream $repl

                    $master debug set-active-expire 0
                }

                test {module propagation with notifications with eviction case 1} {
                    $master flushall
                    $master set asdf1 1
                    $master set asdf2 2
                    $master set asdf3 3

                    $master config set maxmemory-policy allkeys-random
                    $master config set maxmemory 1

                    # Please note the following loop:
                    # We evict a key and send a notification, which does INCR on the "notifications" key, so
                    # that every time we evict any key, "notifications" key exist (it happens inside the
                    # performEvictions loop). So even evicting "notifications" causes INCR on "notifications".
                    # If maxmemory_eviction_tenacity would have been set to 100 this would be an endless loop, but
                    # since the default is 10, at some point the performEvictions loop would end.
                    # Bottom line: "notifications" always exists and we can't really determine the order of evictions
                    # This test is here only for sanity

                    # The replica will get the notification with multi exec and we have a generic notification handler
                    # that performs `RedictModule_Call(ctx, "INCR", "c", "multi");` if the notification is inside multi exec.
                    # so we will have 2 keys, "notifications" and "multi".
                    wait_for_condition 500 10 {
                        [$replica dbsize] eq 2
                    } else {
                        fail "Not all keys have been evicted"
                    }

                    $master config set maxmemory 0
                    $master config set maxmemory-policy noeviction
                }

                test {module propagation with notifications with eviction case 2} {
                    $master flushall
                    set repl [attach_to_replication_stream]

                    $master set asdf1 1 EX 300
                    $master set asdf2 2 EX 300
                    $master set asdf3 3 EX 300

                    # Please note we use volatile eviction to prevent the loop described in the test above.
                    # "notifications" is not volatile so it always remains
                    $master config resetstat
                    $master config set maxmemory-policy volatile-ttl
                    $master config set maxmemory 1

                    wait_for_condition 500 10 {
                        [s evicted_keys] eq 3
                    } else {
                        fail "Not all keys have been evicted"
                    }

                    $master config set maxmemory 0
                    $master config set maxmemory-policy noeviction

                    $master set asdf4 4

                    # Note whenever there's double notification: SET with EX issues two separate
                    # notifications: one for "set" and one for "expire"
                    # Note that although CONFIG SET maxmemory is called in this flow (see issue #10014),
                    # eviction will happen and will not induce propagation of the CONFIG command (see #10019).
                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {incr notifications}
                        {set asdf1 1 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {set asdf2 2 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {set asdf3 3 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {del asdf*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {del asdf*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {del asdf*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {set asdf4 4}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagation with timer and CONFIG SET maxmemory} {
                    set repl [attach_to_replication_stream]

                    $master config resetstat
                    $master config set maxmemory-policy volatile-random

                    $master propagate-test.timer-maxmemory

                    # Wait until the volatile keys are evicted
                    wait_for_condition 500 10 {
                        [s evicted_keys] eq 2
                    } else {
                        fail "Not all keys have been evicted"
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {incr notifications}
                        {set timer-maxmemory-volatile-start 1 PXAT *}
                        {incr timer-maxmemory-middle}
                        {incr notifications}
                        {incr notifications}
                        {set timer-maxmemory-volatile-end 1 PXAT *}
                        {exec}
                        {multi}
                        {incr notifications}
                        {del timer-maxmemory-volatile-*}
                        {exec}
                        {multi}
                        {incr notifications}
                        {del timer-maxmemory-volatile-*}
                        {exec}
                    }
                    close_replication_stream $repl

                    $master config set maxmemory 0
                    $master config set maxmemory-policy noeviction
                }

                test {module propagation with timer and EVAL} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.timer-eval

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {incrby timer-eval-start 1}
                        {incr notifications}
                        {set foo bar}
                        {incr timer-eval-middle}
                        {incr notifications}
                        {incrby timer-eval-end 1}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates nested ctx case1} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.timer-nested

                    wait_for_condition 500 10 {
                        [$replica get timer-nested-end] eq "1"
                    } else {
                        fail "The two counters don't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incrby timer-nested-start 1}
                        {incrby timer-nested-end 1}
                        {exec}
                    }
                    close_replication_stream $repl

                    # Note propagate-test.timer-nested just propagates INCRBY, causing an
                    # inconsistency, so we flush
                    $master flushall
                }

                test {module propagates nested ctx case2} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.timer-nested-repl

                    wait_for_condition 500 10 {
                        [$replica get timer-nested-end] eq "1"
                    } else {
                        fail "The two counters don't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incrby timer-nested-start 1}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr counter-3}
                        {incr counter-4}
                        {incr notifications}
                        {incr after-call}
                        {incr notifications}
                        {incr before-call-2}
                        {incr notifications}
                        {incr asdf}
                        {incr notifications}
                        {del asdf}
                        {incr notifications}
                        {incr after-call-2}
                        {incr notifications}
                        {incr timer-nested-middle}
                        {incrby timer-nested-end 1}
                        {exec}
                    }
                    close_replication_stream $repl

                    # Note propagate-test.timer-nested-repl just propagates INCRBY, causing an
                    # inconsistency, so we flush
                    $master flushall
                }

                test {module propagates from thread} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.thread

                    wait_for_condition 500 10 {
                        [$replica get a-from-thread] eq "3"
                    } else {
                        fail "The two counters don't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr a-from-thread}
                        {incr notifications}
                        {incr thread-call}
                        {incr b-from-thread}
                        {exec}
                        {multi}
                        {incr a-from-thread}
                        {incr notifications}
                        {incr thread-call}
                        {incr b-from-thread}
                        {exec}
                        {multi}
                        {incr a-from-thread}
                        {incr notifications}
                        {incr thread-call}
                        {incr b-from-thread}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from thread with detached ctx} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.detached-thread

                    wait_for_condition 500 10 {
                        [$replica get thread-detached-after] eq "1"
                    } else {
                        fail "The key doesn't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr thread-detached-before}
                        {incr notifications}
                        {incr thread-detached-1}
                        {incr notifications}
                        {incr thread-detached-2}
                        {incr thread-detached-after}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from command} {
                    set repl [attach_to_replication_stream]

                    $master propagate-test.simple
                    $master propagate-test.mixed

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr counter-1}
                        {incr counter-2}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr after-call}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from EVAL} {
                    set repl [attach_to_replication_stream]

                    assert_equal [ $master eval { \
                        redict.call("propagate-test.simple"); \
                        redict.call("set", "x", "y"); \
                        redict.call("propagate-test.mixed"); return "OK" } 0 ] {OK}

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {set x y}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr after-call}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from command after good EVAL} {
                    set repl [attach_to_replication_stream]

                    assert_equal [ $master eval { return "hello" } 0 ] {hello}
                    $master propagate-test.simple
                    $master propagate-test.mixed

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr counter-1}
                        {incr counter-2}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr after-call}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from command after bad EVAL} {
                    set repl [attach_to_replication_stream]

                    catch { $master eval { return "hello" } -12 } e
                    assert_equal $e {ERR Number of keys can't be negative}
                    $master propagate-test.simple
                    $master propagate-test.mixed

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr counter-1}
                        {incr counter-2}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr after-call}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module propagates from multi-exec} {
                    set repl [attach_to_replication_stream]

                    $master multi
                    $master propagate-test.simple
                    $master propagate-test.mixed
                    $master propagate-test.timer-nested-repl
                    $master exec

                    wait_for_condition 500 10 {
                        [$replica get timer-nested-end] eq "1"
                    } else {
                        fail "The two counters don't match the expected value."
                    }

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr notifications}
                        {incr after-call}
                        {exec}
                        {multi}
                        {incrby timer-nested-start 1}
                        {incr notifications}
                        {incr using-call}
                        {incr counter-1}
                        {incr counter-2}
                        {incr counter-3}
                        {incr counter-4}
                        {incr notifications}
                        {incr after-call}
                        {incr notifications}
                        {incr before-call-2}
                        {incr notifications}
                        {incr asdf}
                        {incr notifications}
                        {del asdf}
                        {incr notifications}
                        {incr after-call-2}
                        {incr notifications}
                        {incr timer-nested-middle}
                        {incrby timer-nested-end 1}
                        {exec}
                    }
                    close_replication_stream $repl

                   # Note propagate-test.timer-nested just propagates INCRBY, causing an
                    # inconsistency, so we flush
                    $master flushall
                }

                test {module RM_Call of expired key propagation} {
                    $master debug set-active-expire 0

                    $master set k1 900 px 100
                    after 110

                    set repl [attach_to_replication_stream]
                    $master propagate-test.incr k1

                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {del k1}
                        {propagate-test.incr k1}
                        {exec}
                    }
                    close_replication_stream $repl

                    assert_equal [$master get k1] 1
                    assert_equal [$master ttl k1] -1
                    assert_equal [$replica get k1] 1
                    assert_equal [$replica ttl k1] -1
                }

                test {module notification on set} {
                    set repl [attach_to_replication_stream]

                    $master SADD s foo

                    wait_for_condition 500 10 {
                        [$replica SCARD s] eq "1"
                    } else {
                        fail "Failed to wait for set to be replicated"
                    }

                    $master SPOP s 1

                    wait_for_condition 500 10 {
                        [$replica SCARD s] eq "0"
                    } else {
                        fail "Failed to wait for set to be replicated"
                    }

                    # Currently the `del` command comes after the notification.
                    # When we fix spop to fire notification at the end (like all other commands),
                    # the `del` will come first.
                    assert_replication_stream $repl {
                        {multi}
                        {select *}
                        {incr notifications}
                        {sadd s foo}
                        {exec}
                        {multi}
                        {incr notifications}
                        {incr notifications}
                        {del s}
                        {exec}
                    }
                    close_replication_stream $repl
                }

                test {module key miss notification do not cause read command to be replicated} {
                    set repl [attach_to_replication_stream]

                    $master flushall

                    $master get unexisting_key

                    wait_for_condition 500 10 {
                        [$replica get missed] eq "1"
                    } else {
                        fail "Failed to wait for set to be replicated"
                    }

                    # Test is checking a wrong!!! behavior that causes a read command to be replicated to replica/aof.
                    # We keep the test to verify that such a wrong behavior does not cause any crashes.
                    assert_replication_stream $repl {
                        {select *}
                        {flushall}
                        {multi}
                        {incr notifications}
                        {incr missed}
                        {get unexisting_key}
                        {exec}
                    }

                    close_replication_stream $repl
                }

                test "Unload the module - propagate-test/testkeyspace" {
                    assert_equal {OK} [r module unload propagate-test]
                    assert_equal {OK} [r module unload testkeyspace]
                }

                assert_equal [s -1 unexpected_error_replies] 0
            }
        }
    }
}


tags "modules aof" {
    foreach aofload_type {debug_cmd startup} {
    test "Modules RM_Replicate replicates MULTI/EXEC correctly: AOF-load type $aofload_type" {
        start_server [list overrides [list loadmodule "$testmodule"]] {
            # Enable the AOF
            r config set appendonly yes
            r config set auto-aof-rewrite-percentage 0 ; # Disable auto-rewrite.
            waitForBgrewriteaof r

            r propagate-test.simple
            r propagate-test.mixed
            r multi
            r propagate-test.simple
            r propagate-test.mixed
            r exec

            assert_equal [r get counter-1] {}
            assert_equal [r get counter-2] {}
            assert_equal [r get using-call] 2
            assert_equal [r get after-call] 2
            assert_equal [r get notifications] 4

            # Load the AOF
            if {$aofload_type == "debug_cmd"} {
                r debug loadaof
            } else {
                r config rewrite
                restart_server 0 true false
                wait_done_loading r
            }

            # This module behaves bad on purpose, it only calls
            # RM_Replicate for counter-1 and counter-2 so values
            # after AOF-load are different
            assert_equal [r get counter-1] 4
            assert_equal [r get counter-2] 4
            assert_equal [r get using-call] 2
            assert_equal [r get after-call] 2
            # 4+4+2+2 commands from AOF (just above) + 4 "INCR notifications" from AOF + 4 notifications for these INCRs
            assert_equal [r get notifications] 20

            assert_equal {OK} [r module unload propagate-test]
            assert_equal [s 0 unexpected_error_replies] 0
        }
    }
    test "Modules RM_Call does not update stats during aof load: AOF-load type $aofload_type" {
        start_server [list overrides [list loadmodule "$miscmodule"]] {
            # Enable the AOF
            r config set appendonly yes
            r config set auto-aof-rewrite-percentage 0 ; # Disable auto-rewrite.
            waitForBgrewriteaof r

            r config resetstat
            r set foo bar
            r EVAL {return redict.call('SET', KEYS[1], ARGV[1])} 1 foo bar2
            r test.rm_call_replicate set foo bar3
            r EVAL {return redict.call('test.rm_call_replicate',ARGV[1],KEYS[1],ARGV[2])} 1 foo set bar4

            r multi
            r set foo bar5
            r EVAL {return redict.call('SET', KEYS[1], ARGV[1])} 1 foo bar6
            r test.rm_call_replicate set foo bar7
            r EVAL {return redict.call('test.rm_call_replicate',ARGV[1],KEYS[1],ARGV[2])} 1 foo set bar8
            r exec

            assert_match {*calls=8,*,rejected_calls=0,failed_calls=0} [cmdrstat set r]


            # Load the AOF
            if {$aofload_type == "debug_cmd"} {
                r config resetstat
                r debug loadaof
            } else {
                r config rewrite
                restart_server 0 true false
                wait_done_loading r
            }

            assert_no_match {*calls=*} [cmdrstat set r]

        }
    }
    }
}
