# 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

# This module can be configure with multiple options given as flags on module load time
# 0 - not aux fields will be declared (this is the default)
# 1 << 0 - use aux_save2 api
# 1 << 1 - call aux callback before key space
# 1 << 2 - call aux callback after key space
# 1 << 3 - do not save data on aux callback
set testmodule [file normalize tests/modules/testrdb.so]

tags "modules" {
    test {modules are able to persist types} {
        start_server [list overrides [list loadmodule "$testmodule"]] {
            r testrdb.set.key key1 value1
            assert_equal "value1" [r testrdb.get.key key1]
            r debug reload
            assert_equal "value1" [r testrdb.get.key key1]
        }
    }

    test {modules global are lost without aux} {
        set server_path [tmpdir "server.module-testrdb"]
        start_server [list overrides [list loadmodule "$testmodule" "dir" $server_path] keep_persistence true] {
            r testrdb.set.before global1
            assert_equal "global1" [r testrdb.get.before]
        }
        start_server [list overrides [list loadmodule "$testmodule" "dir" $server_path]] {
            assert_equal "" [r testrdb.get.before]
        }
    }

    test {aux that saves no data are not saved to the rdb when aux_save2 is used} {
        set server_path [tmpdir "server.module-testrdb"]
        puts $server_path
        # 15 == 1111 - use aux_save2 before and after key space without data
        start_server [list overrides [list loadmodule "$testmodule 15" "dir" $server_path] keep_persistence true] {
            r set x 1
            r save
        }
        start_server [list overrides [list "dir" $server_path] keep_persistence true] {
            # make sure server started successfully without the module.
            assert_equal {1} [r get x]
        }
    }

    test {aux that saves no data are saved to the rdb when aux_save is used} {
        set server_path [tmpdir "server.module-testrdb"]
        puts $server_path
        # 14 == 1110 - use aux_save before and after key space without data
        start_server [list overrides [list loadmodule "$testmodule 14" "dir" $server_path] keep_persistence true] {
            r set x 1
            r save
        }
        start_server [list overrides [list loadmodule "$testmodule 14" "dir" $server_path] keep_persistence true] {
            # make sure server started successfully and aux_save was called twice.
            assert_equal {1} [r get x]
            assert_equal {2} [r testrdb.get.n_aux_load_called]
        }
    }

    foreach test_case {6 7} {
    # 6 == 0110 - use aux_save before and after key space with data
    # 7 == 0111 - use aux_save2 before and after key space with data
    test {modules are able to persist globals before and after} {
        set server_path [tmpdir "server.module-testrdb"]
        start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path "save" "900 1"] keep_persistence true] {
            r testrdb.set.before global1
            r testrdb.set.after global2
            assert_equal "global1" [r testrdb.get.before]
            assert_equal "global2" [r testrdb.get.after]
        }
        start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path "save" "900 1"]] {
            assert_equal "global1" [r testrdb.get.before]
            assert_equal "global2" [r testrdb.get.after]
        }

    }
    }

    foreach test_case {4 5} {
    # 4 == 0100 - use aux_save after key space with data
    # 5 == 0101 - use aux_save2 after key space with data
    test {modules are able to persist globals just after} {
        set server_path [tmpdir "server.module-testrdb"]
        start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path "save" "900 1"] keep_persistence true] {
            r testrdb.set.after global2
            assert_equal "global2" [r testrdb.get.after]
        }
        start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path "save" "900 1"]] {
            assert_equal "global2" [r testrdb.get.after]
        }
    }
    }

    test {Verify module options info} {
        start_server [list overrides [list loadmodule "$testmodule"]] {
            assert_match "*\[handle-io-errors|handle-repl-async-load\]*" [r info modules]
        }
    }

    tags {repl} {
        test {diskless loading short read with module} {
            start_server [list overrides [list loadmodule "$testmodule"]] {
                set replica [srv 0 client]
                set replica_host [srv 0 host]
                set replica_port [srv 0 port]
                start_server [list overrides [list loadmodule "$testmodule"]] {
                    set master [srv 0 client]
                    set master_host [srv 0 host]
                    set master_port [srv 0 port]

                    # Set master and replica to use diskless replication
                    $master config set repl-diskless-sync yes
                    $master config set rdbcompression no
                    $replica config set repl-diskless-load swapdb
                    $master config set hz 500
                    $replica config set hz 500
                    $master config set dynamic-hz no
                    $replica config set dynamic-hz no
                    set start [clock clicks -milliseconds]
                    for {set k 0} {$k < 30} {incr k} {
                        r testrdb.set.key key$k [string repeat A [expr {int(rand()*1000000)}]]
                    }

                    if {$::verbose} {
                        set end [clock clicks -milliseconds]
                        set duration [expr $end - $start]
                        puts "filling took $duration ms (TODO: use pipeline)"
                        set start [clock clicks -milliseconds]
                    }

                    # Start the replication process...
                    set loglines [count_log_lines -1]
                    $master config set repl-diskless-sync-delay 0
                    $replica replicaof $master_host $master_port

                    # kill the replication at various points
                    set attempts 100
                    if {$::accurate} { set attempts 500 }
                    for {set i 0} {$i < $attempts} {incr i} {
                        # wait for the replica to start reading the rdb
                        # using the log file since the replica only responds to INFO once in 2mb
                        set res [wait_for_log_messages -1 {"*Loading DB in memory*"} $loglines 2000 1]
                        set loglines [lindex $res 1]

                        # add some additional random sleep so that we kill the master on a different place each time
                        after [expr {int(rand()*50)}]

                        # kill the replica connection on the master
                        set killed [$master client kill type replica]

                        set res [wait_for_log_messages -1 {"*Internal error in RDB*" "*Finished with success*" "*Successful partial resynchronization*"} $loglines 500 10]
                        if {$::verbose} { puts $res }
                        set log_text [lindex $res 0]
                        set loglines [lindex $res 1]
                        if {![string match "*Internal error in RDB*" $log_text]} {
                            # force the replica to try another full sync
                            $master multi
                            $master client kill type replica
                            $master set asdf asdf
                            # fill replication backlog with new content
                            $master config set repl-backlog-size 16384
                            for {set keyid 0} {$keyid < 10} {incr keyid} {
                                $master set "$keyid string_$keyid" [string repeat A 16384]
                            }
                            $master exec
                        }

                        # wait for loading to stop (fail)
                        # After a loading successfully, next loop will enter `async_loading`
                        wait_for_condition 1000 1 {
                            [s -1 async_loading] eq 0 &&
                            [s -1 loading] eq 0
                        } else {
                            fail "Replica didn't disconnect"
                        }
                    }
                    if {$::verbose} {
                        set end [clock clicks -milliseconds]
                        set duration [expr $end - $start]
                        puts "test took $duration ms"
                    }
                    # enable fast shutdown
                    $master config set rdb-key-save-delay 0
                }
            }
        }

        # Module events for diskless load swapdb when async_loading (matching master replid)
        foreach test_case {6 7} {
        # 6 == 0110 - use aux_save before and after key space with data
        # 7 == 0111 - use aux_save2 before and after key space with data
        foreach testType {Successful Aborted} {
            start_server [list overrides [list loadmodule "$testmodule $test_case"] tags [list external:skip]] {
                set replica [srv 0 client]
                set replica_host [srv 0 host]
                set replica_port [srv 0 port]
                set replica_log [srv 0 stdout]
                start_server [list overrides [list loadmodule "$testmodule $test_case"]] {
                    set master [srv 0 client]
                    set master_host [srv 0 host]
                    set master_port [srv 0 port]

                    set start [clock clicks -milliseconds]

                    # Set master and replica to use diskless replication on swapdb mode
                    $master config set repl-diskless-sync yes
                    $master config set repl-diskless-sync-delay 0
                    $master config set save ""
                    $replica config set repl-diskless-load swapdb
                    $replica config set save ""

                    # Initial sync to have matching replids between master and replica
                    $replica replicaof $master_host $master_port

                    # Let replica finish initial sync with master
                    wait_for_condition 100 100 {
                        [s -1 master_link_status] eq "up"
                    } else {
                        fail "Master <-> Replica didn't finish sync"
                    }

                    # Set global values on module so we can check if module event callbacks will pick it up correctly
                    $master testrdb.set.before value1_master
                    $replica testrdb.set.before value1_replica

                    # Put different data sets on the master and replica
                    # We need to put large keys on the master since the replica replies to info only once in 2mb
                    $replica debug populate 200 slave 10
                    $master debug populate 1000 master 100000
                    $master config set rdbcompression no

                    # Force the replica to try another full sync (this time it will have matching master replid)
                    $master multi
                    $master client kill type replica
                    # Fill replication backlog with new content
                    $master config set repl-backlog-size 16384
                    for {set keyid 0} {$keyid < 10} {incr keyid} {
                        $master set "$keyid string_$keyid" [string repeat A 16384]
                    }
                    $master exec

                    switch $testType {
                        "Aborted" {
                            # Set master with a slow rdb generation, so that we can easily intercept loading
                            # 10ms per key, with 1000 keys is 10 seconds
                            $master config set rdb-key-save-delay 10000

                            test {Diskless load swapdb RedictModuleEvent_ReplAsyncLoad handling: during loading, can keep module variable same as before} {
                                # Wait for the replica to start reading the rdb and module for acknowledgement
                                # We wanna abort only after the temp db was populated by REDICTMODULE_AUX_BEFORE_RDB
                                wait_for_condition 100 100 {
                                    [s -1 async_loading] eq 1 && [$replica testrdb.async_loading.get.before] eq "value1_master"
                                } else {
                                    fail "Module didn't receive or react to REDICTMODULE_SUBEVENT_REPL_ASYNC_LOAD_STARTED"
                                }

                                assert_equal [$replica dbsize] 200
                                assert_equal value1_replica [$replica testrdb.get.before]
                            }

                            # Make sure that next sync will not start immediately so that we can catch the replica in between syncs
                            $master config set repl-diskless-sync-delay 5

                            # Kill the replica connection on the master
                            set killed [$master client kill type replica]

                            test {Diskless load swapdb RedictModuleEvent_ReplAsyncLoad handling: when loading aborted, can keep module variable same as before} {
                                # Wait for loading to stop (fail) and module for acknowledgement
                                wait_for_condition 100 100 {
                                    [s -1 async_loading] eq 0 && [$replica testrdb.async_loading.get.before] eq ""
                                } else {
                                    fail "Module didn't receive or react to REDICTMODULE_SUBEVENT_REPL_ASYNC_LOAD_ABORTED"
                                }

                                assert_equal [$replica dbsize] 200
                                assert_equal value1_replica [$replica testrdb.get.before]
                            }

                            # Speed up shutdown
                            $master config set rdb-key-save-delay 0
                        }
                        "Successful" {
                            # Let replica finish sync with master
                            wait_for_condition 100 100 {
                                [s -1 master_link_status] eq "up"
                            } else {
                                fail "Master <-> Replica didn't finish sync"
                            }

                            test {Diskless load swapdb RedictModuleEvent_ReplAsyncLoad handling: after db loaded, can set module variable with new value} {
                                assert_equal [$replica dbsize] 1010
                                assert_equal value1_master [$replica testrdb.get.before]
                            }
                        }
                    }

                    if {$::verbose} {
                        set end [clock clicks -milliseconds]
                        set duration [expr $end - $start]
                        puts "test took $duration ms"
                    }
                }
            }
        }
        }
    }
}
