# 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

proc get_function_code {args} {
    return [format "#!%s name=%s\nredict.register_function('%s', function(KEYS, ARGV)\n %s \nend)" [lindex $args 0] [lindex $args 1] [lindex $args 2] [lindex $args 3]]
}

proc get_no_writes_function_code {args} {
    return [format "#!%s name=%s\nredict.register_function{function_name='%s', callback=function(KEYS, ARGV)\n %s \nend, flags={'no-writes'}}" [lindex $args 0] [lindex $args 1] [lindex $args 2] [lindex $args 3]]
}

start_server {tags {"scripting"}} {
    test {FUNCTION - Basic usage} {
        r function load [get_function_code LUA test test {return 'hello'}]
        r fcall test 0
    } {hello}

    test {FUNCTION - Load with unknown argument} {
        catch {
            r function load foo bar [get_function_code LUA test test {return 'hello'}]
        } e
        set _ $e
    } {*Unknown option given*}

    test {FUNCTION - Create an already exiting library raise error} {
        catch {
            r function load [get_function_code LUA test test {return 'hello1'}]
        } e
        set _ $e
    } {*already exists*}

    test {FUNCTION - Create an already exiting library raise error (case insensitive)} {
        catch {
            r function load [get_function_code LUA test test {return 'hello1'}]
        } e
        set _ $e
    } {*already exists*}

    test {FUNCTION - Create a library with wrong name format} {
        catch {
            r function load [get_function_code LUA {bad\0foramat} test {return 'hello1'}]
        } e
        set _ $e
    } {*Library names can only contain letters, numbers, or underscores(_)*}

    test {FUNCTION - Create library with unexisting engine} {
        catch {
            r function load [get_function_code bad_engine test test {return 'hello1'}]
        } e
        set _ $e
    } {*Engine 'bad_engine' not found*}

    test {FUNCTION - Test uncompiled script} {
        catch {
            r function load replace [get_function_code LUA test test {bad script}]
        } e
        set _ $e
    } {*Error compiling function*}

    test {FUNCTION - test replace argument} {
        r function load REPLACE [get_function_code LUA test test {return 'hello1'}]
        r fcall test 0
    } {hello1}

    test {FUNCTION - test function case insensitive} {
        r fcall TEST 0
    } {hello1}

    test {FUNCTION - test replace argument with failure keeps old libraries} {
        catch {r function load REPLACE [get_function_code LUA test test {error}]} e
        assert_match {ERR Error compiling function*} $e
        r fcall test 0
    } {hello1}

    test {FUNCTION - test function delete} {
        r function delete test
        catch {
            r fcall test 0
        } e
        set _ $e
    } {*Function not found*}

    test {FUNCTION - test fcall bad arguments} {
        r function load [get_function_code LUA test test {return 'hello'}]
        catch {
            r fcall test bad_arg
        } e
        set _ $e
    } {*Bad number of keys provided*}

    test {FUNCTION - test fcall bad number of keys arguments} {
        catch {
            r fcall test 10 key1
        } e
        set _ $e
    } {*Number of keys can't be greater than number of args*}

    test {FUNCTION - test fcall negative number of keys} {
        catch {
            r fcall test -1 key1
        } e
        set _ $e
    } {*Number of keys can't be negative*}

    test {FUNCTION - test delete on not exiting library} {
        catch {
            r function delete test1
        } e
        set _ $e
    } {*Library not found*}

    test {FUNCTION - test function kill when function is not running} {
        catch {
            r function kill
        } e
        set _ $e
    } {*No scripts in execution*}

    test {FUNCTION - test wrong subcommand} {
        catch {
            r function bad_subcommand
        } e
        set _ $e
    } {*unknown subcommand*}

    test {FUNCTION - test loading from rdb} {
        r debug reload
        r fcall test 0
    } {hello} {needs:debug}

    test {FUNCTION - test debug reload different options} {
        catch {r debug reload noflush} e
        assert_match "*Error trying to load the RDB*" $e
        r debug reload noflush merge
        r function list
    } {{library_name test engine LUA functions {{name test description {} flags {}}}}} {needs:debug}

    test {FUNCTION - test debug reload with nosave and noflush} {
        r function delete test
        r set x 1
        r function load [get_function_code LUA test1 test1 {return 'hello'}]
        r debug reload
        r function load [get_function_code LUA test2 test2 {return 'hello'}]
        r debug reload nosave noflush merge
        assert_equal [r fcall test1 0] {hello}
        assert_equal [r fcall test2 0] {hello}
    } {} {needs:debug}

    test {FUNCTION - test flushall and flushdb do not clean functions} {
        r function flush
        r function load REPLACE [get_function_code lua test test {return redict.call('set', 'x', '1')}]
        r flushall
        r flushdb
        r function list
    } {{library_name test engine LUA functions {{name test description {} flags {}}}}}

    test {FUNCTION - test function dump and restore} {
        r function flush
        r function load [get_function_code lua test test {return 'hello'}]
        set e [r function dump]
        r function delete test
        assert_match {} [r function list]
        r function restore $e
        r function list
    } {{library_name test engine LUA functions {{name test description {} flags {}}}}}

    test {FUNCTION - test function dump and restore with flush argument} {
        set e [r function dump]
        r function flush
        assert_match {} [r function list]
        r function restore $e FLUSH
        r function list
    } {{library_name test engine LUA functions {{name test description {} flags {}}}}}

    test {FUNCTION - test function dump and restore with append argument} {
        set e [r function dump]
        r function flush
        assert_match {} [r function list]
        r function load [get_function_code lua test test {return 'hello1'}]
        catch {r function restore $e APPEND} err
        assert_match {*already exists*} $err
        r function flush
        r function load [get_function_code lua test1 test1 {return 'hello1'}]
        r function restore $e APPEND
        assert_match {hello} [r fcall test 0]
        assert_match {hello1} [r fcall test1 0]
    }

    test {FUNCTION - test function dump and restore with replace argument} {
        r function flush
        r function load [get_function_code LUA test test {return 'hello'}]
        set e [r function dump]
        r function flush
        assert_match {} [r function list]
        r function load [get_function_code lua test test {return 'hello1'}]
        assert_match {hello1} [r fcall test 0]
        r function restore $e REPLACE
        assert_match {hello} [r fcall test 0]
    }

    test {FUNCTION - test function restore with bad payload do not drop existing functions} {
        r function flush
        r function load [get_function_code LUA test test {return 'hello'}]
        catch {r function restore bad_payload} e
        assert_match {*payload version or checksum are wrong*} $e
        r function list
    } {{library_name test engine LUA functions {{name test description {} flags {}}}}}

    test {FUNCTION - test function restore with wrong number of arguments} {
        catch {r function restore arg1 args2 arg3} e
        set _ $e
    } {*unknown subcommand or wrong number of arguments for 'restore'. Try FUNCTION HELP.}

    test {FUNCTION - test fcall_ro with write command} {
        r function load REPLACE [get_no_writes_function_code lua test test {return redict.call('set', 'x', '1')}]
        catch { r fcall_ro test 1 x } e
        set _ $e
    } {*Write commands are not allowed from read-only scripts*}

    test {FUNCTION - test fcall_ro with read only commands} {
        r function load REPLACE [get_no_writes_function_code lua test test {return redict.call('get', 'x')}]
        r set x 1
        r fcall_ro test 1 x
    } {1}

    test {FUNCTION - test keys and argv} {
        r function load REPLACE [get_function_code lua test test {return redict.call('set', KEYS[1], ARGV[1])}]
        r fcall test 1 x foo
        r get x
    } {foo}

    test {FUNCTION - test command get keys on fcall} {
        r COMMAND GETKEYS fcall test 1 x foo
    } {x}

    test {FUNCTION - test command get keys on fcall_ro} {
        r COMMAND GETKEYS fcall_ro test 1 x foo
    } {x}

    test {FUNCTION - test function kill} {
        set rd [redict_deferring_client]
        r config set busy-reply-threshold 10
        r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
        $rd fcall test 0
        after 200
        catch {r ping} e
        assert_match {BUSY*} $e
        assert_match {running_script {name test command {fcall test 0} duration_ms *} engines {*}} [r FUNCTION STATS]
        r function kill
        after 200 ; # Give some time to Lua to call the hook again...
        assert_equal [r ping] "PONG"
    }

    test {FUNCTION - test script kill not working on function} {
        set rd [redict_deferring_client]
        r config set busy-reply-threshold 10
        r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
        $rd fcall test 0
        after 200
        catch {r ping} e
        assert_match {BUSY*} $e
        catch {r script kill} e
        assert_match {BUSY*} $e
        r function kill
        after 200 ; # Give some time to Lua to call the hook again...
        assert_equal [r ping] "PONG"
    }

    test {FUNCTION - test function kill not working on eval} {
        set rd [redict_deferring_client]
        r config set busy-reply-threshold 10
        $rd eval {local a = 1 while true do a = a + 1 end} 0
        after 200
        catch {r ping} e
        assert_match {BUSY*} $e
        catch {r function kill} e
        assert_match {BUSY*} $e
        r script kill
        after 200 ; # Give some time to Lua to call the hook again...
        assert_equal [r ping] "PONG"
    }

    test {FUNCTION - test function flush} {
        r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
        assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
        r function flush
        assert_match {} [r function list]

        r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
        assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
        r function flush async
        assert_match {} [r function list]

        r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
        assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
        r function flush sync
        assert_match {} [r function list]
    }

    test {FUNCTION - test function wrong argument} {
        catch {r function flush bad_arg} e
        assert_match {*only supports SYNC|ASYNC*} $e

        catch {r function flush sync extra_arg} e
        assert_match {*unknown subcommand or wrong number of arguments for 'flush'. Try FUNCTION HELP.} $e
    }
}

start_server {tags {"scripting repl external:skip"}} {
    start_server {} {
        test "Connect a replica to the master instance" {
            r -1 slaveof [srv 0 host] [srv 0 port]
            wait_for_condition 150 100 {
                [s -1 role] eq {slave} &&
                [string match {*master_link_status:up*} [r -1 info replication]]
            } else {
                fail "Can't turn the instance into a replica"
            }
        }

        test {FUNCTION - creation is replicated to replica} {
            r function load [get_no_writes_function_code LUA test test {return 'hello'}]
            wait_for_condition 150 100 {
                [r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }
        }

        test {FUNCTION - call on replica} {
            r -1 fcall test 0
        } {hello}

        test {FUNCTION - restore is replicated to replica} {
            set e [r function dump]

            r function delete test
            wait_for_condition 150 100 {
                [r -1 function list] eq {}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }

            assert_equal [r function restore $e] {OK}

            wait_for_condition 150 100 {
                [r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }
        }

        test {FUNCTION - delete is replicated to replica} {
            r function delete test
            wait_for_condition 150 100 {
                [r -1 function list] eq {}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }
        }

        test {FUNCTION - flush is replicated to replica} {
            r function load [get_function_code LUA test test {return 'hello'}]
            wait_for_condition 150 100 {
                [r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags {}}}}}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }
            r function flush
            wait_for_condition 150 100 {
                [r -1 function list] eq {}
            } else {
                fail "Failed waiting for function to replicate to replica"
            }
        }

        test "Disconnecting the replica from master instance" {
            r -1 slaveof no one
            # creating a function after disconnect to make sure function
            # is replicated on rdb phase
            r function load [get_no_writes_function_code LUA test test {return 'hello'}]

            # reconnect the replica
            r -1 slaveof [srv 0 host] [srv 0 port]
            wait_for_condition 150 100 {
                [s -1 role] eq {slave} &&
                [string match {*master_link_status:up*} [r -1 info replication]]
            } else {
                fail "Can't turn the instance into a replica"
            }
        }

        test "FUNCTION - test replication to replica on rdb phase" {
            r -1 fcall test 0
        } {hello}

        test "FUNCTION - test replication to replica on rdb phase info command" {
            r -1 function list
        } {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}

        test "FUNCTION - create on read only replica" {
            catch {
                r -1 function load [get_function_code LUA test test {return 'hello'}]
            } e
            set _ $e
        } {*can't write against a read only replica*}

        test "FUNCTION - delete on read only replica" {
            catch {
                r -1 function delete test
            } e
            set _ $e
        } {*can't write against a read only replica*}

        test "FUNCTION - function effect is replicated to replica" {
            r function load REPLACE [get_function_code LUA test test {return redict.call('set', 'x', '1')}]
            r fcall test 1 x
            assert {[r get x] eq {1}}
            wait_for_condition 150 100 {
                [r -1 get x] eq {1}
            } else {
                fail "Failed waiting function effect to be replicated to replica"
            }
        }

        test "FUNCTION - modify key space of read only replica" {
            catch {
                r -1 fcall test 1 x
            } e
            set _ $e
        } {READONLY You can't write against a read only replica.}
    }
}

test {FUNCTION can processes create, delete and flush commands in AOF when doing "debug loadaof" in read-only slaves} {
    start_server {} {
        r config set appendonly yes
        waitForBgrewriteaof r
        r FUNCTION LOAD "#!lua name=test\nredict.register_function('test', function() return 'hello' end)"
        r config set slave-read-only yes
        r slaveof 127.0.0.1 0
        r debug loadaof
        r slaveof no one
        assert_equal [r function list] {{library_name test engine LUA functions {{name test description {} flags {}}}}}

        r FUNCTION DELETE test

        r slaveof 127.0.0.1 0
        r debug loadaof
        r slaveof no one
        assert_equal [r function list] {}

        r FUNCTION LOAD "#!lua name=test\nredict.register_function('test', function() return 'hello' end)"
        r FUNCTION FLUSH

        r slaveof 127.0.0.1 0
        r debug loadaof
        r slaveof no one
        assert_equal [r function list] {}
    }
} {} {needs:debug external:skip}

start_server {tags {"scripting"}} {
    test {LIBRARIES - test shared function can access default globals} {
        r function load {#!lua name=lib1
            local function ping()
                return redict.call('ping')
            end
            redict.register_function(
                'f1',
                function(keys, args)
                    return ping()
                end
            )
        }
        r fcall f1 0
    } {PONG}

    test {LIBRARIES - usage and code sharing} {
        r function load REPLACE {#!lua name=lib1
            local function add1(a)
                return a + 1
            end
            redict.register_function(
                'f1',
                function(keys, args)
                    return add1(1)
                end
            )
            redict.register_function(
                'f2',
                function(keys, args)
                    return add1(2)
                end
            )
        }
        assert_equal [r fcall f1 0] {2}
        assert_equal [r fcall f2 0] {3}
        r function list
    } {{library_name lib1 engine LUA functions {*}}}

    test {LIBRARIES - test registration failure revert the entire load} {
        catch {
            r function load replace {#!lua name=lib1
                local function add1(a)
                    return a + 2
                end
                redict.register_function(
                    'f1',
                    function(keys, args)
                        return add1(1)
                    end
                )
                redict.register_function(
                    'f2',
                    'not a function'
                )
            }
        } e
        assert_match {*second argument to redict.register_function must be a function*} $e
        assert_equal [r fcall f1 0] {2}
        assert_equal [r fcall f2 0] {3}
    }

    test {LIBRARIES - test registration function name collision} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function(
                    'f1',
                    function(keys, args)
                        return 1
                    end
                )
            }
        } e
        assert_match {*Function f1 already exists*} $e
        assert_equal [r fcall f1 0] {2}
        assert_equal [r fcall f2 0] {3}
    }

    test {LIBRARIES - test registration function name collision on same library} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function(
                    'f1',
                    function(keys, args)
                        return 1
                    end
                )
                redict.register_function(
                    'f1',
                    function(keys, args)
                        return 1
                    end
                )
            }
        } e
        set _ $e
    } {*Function already exists in the library*}

    test {LIBRARIES - test registration with no argument} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function()
            }
        } e
        set _ $e
    } {*wrong number of arguments to redict.register_function*}

    test {LIBRARIES - test registration with only name} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function('f1')
            }
        } e
        set _ $e
    } {*calling redict.register_function with a single argument is only applicable to Lua table*}

    test {LIBRARIES - test registration with to many arguments} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function('f1', function() return 1 end, {}, 'description', 'extra arg')
            }
        } e
        set _ $e
    } {*wrong number of arguments to redict.register_function*}

    test {LIBRARIES - test registration with no string name} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function(nil, function() return 1 end)
            }
        } e
        set _ $e
    } {*first argument to redict.register_function must be a string*}

    test {LIBRARIES - test registration with wrong name format} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function('test\0test', function() return 1 end)
            }
        } e
        set _ $e
    } {*Library names can only contain letters, numbers, or underscores(_) and must be at least one character long*}

    test {LIBRARIES - test registration with empty name} {
        catch {
            r function load replace {#!lua name=lib2
                redict.register_function('', function() return 1 end)
            }
        } e
        set _ $e
    } {*Library names can only contain letters, numbers, or underscores(_) and must be at least one character long*}

    test {LIBRARIES - math.random from function load} {
        catch {
            r function load replace {#!lua name=lib2
                return math.random()
            }
        } e
        set _ $e
    } {*attempted to access nonexistent global variable 'math'*}

    test {LIBRARIES - redict.call from function load} {
        catch {
            r function load replace {#!lua name=lib2
                return redict.call('ping')
            }
        } e
        set _ $e
    } {*attempted to access nonexistent global variable 'call'*}

    test {LIBRARIES - redict.setresp from function load} {
        catch {
            r function load replace {#!lua name=lib2
                return redict.setresp(3)
            }
        } e
        set _ $e
    } {*attempted to access nonexistent global variable 'setresp'*}

    test {LIBRARIES - redict.set_repl from function load} {
        catch {
            r function load replace {#!lua name=lib2
                return redict.set_repl(redict.REPL_NONE)
            }
        } e
        set _ $e
    } {*attempted to access nonexistent global variable 'set_repl'*}

    test {LIBRARIES - redict.acl_check_cmd from function load} {
        catch {
            r function load replace {#!lua name=lib2
                return redict.acl_check_cmd('set','xx',1)
            }
        } e
        set _ $e
    } {*attempted to access nonexistent global variable 'acl_check_cmd'*}

    test {LIBRARIES - malicious access test} {
        # the 'library' API is not exposed inside a
        # function context and the 'redict' API is not
        # expose on the library registration context.
        # But a malicious user might find a way to hack it
        # (as demonstrated in this test). This is why we
        # have another level of protection on the C
        # code itself and we want to test it and verify
        # that it works properly.
        r function load replace {#!lua name=lib1
            local lib = redict
            lib.register_function('f1', function ()
                lib.redict = redict
                lib.math = math
                return {ok='OK'}
            end)

            lib.register_function('f2', function ()
                lib.register_function('f1', function ()
                    lib.redict = redict
                    lib.math = math
                    return {ok='OK'}
                end)
            end)
        }
        catch {[r fcall f1 0]} e
        assert_match {*Attempt to modify a readonly table*} $e

        catch {[r function load {#!lua name=lib2
            redict.math.random()
        }]} e
        assert_match {*Script attempted to access nonexistent global variable 'math'*} $e

        catch {[r function load {#!lua name=lib2
            redict.redict.call('ping')
        }]} e
        assert_match {*Script attempted to access nonexistent global variable 'redict'*} $e

        catch {[r fcall f2 0]} e
        assert_match {*can only be called on FUNCTION LOAD command*} $e
    }

    test {LIBRARIES - delete removed all functions on library} {
        r function delete lib1
        r function list
    } {}

    test {LIBRARIES - register function inside a function} {
        r function load {#!lua name=lib
            redict.register_function(
                'f1',
                function(keys, args)
                    redict.register_function(
                        'f2',
                        function(key, args)
                            return 2
                        end
                    )
                    return 1
                end
            )
        }
        catch {r fcall f1 0} e
        set _ $e
    } {*attempt to call field 'register_function' (a nil value)*}

    test {LIBRARIES - register library with no functions} {
        r function flush
        catch {
            r function load {#!lua name=lib
                return 1
            }
        } e
        set _ $e
    } {*No functions registered*}

    test {LIBRARIES - load timeout} {
        catch {
            r function load {#!lua name=lib
                local a = 1
                while 1 do a = a + 1 end
            }
        } e
        set _ $e
    } {*FUNCTION LOAD timeout*}

    test {LIBRARIES - verify global protection on the load run} {
        catch {
            r function load {#!lua name=lib
                a = 1
            }
        } e
        set _ $e
    } {*Attempt to modify a readonly table*}

    test {LIBRARIES - named arguments} {
        r function load {#!lua name=lib
            redict.register_function{
                function_name='f1',
                callback=function()
                    return 'hello'
                end,
                description='some desc'
            }
        }
        r function list
    } {{library_name lib engine LUA functions {{name f1 description {some desc} flags {}}}}}

    test {LIBRARIES - named arguments, bad function name} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    function_name=function() return 1 end,
                    callback=function()
                        return 'hello'
                    end,
                    description='some desc'
                }
            }
        } e
        set _ $e
    } {*function_name argument given to redict.register_function must be a string*}

    test {LIBRARIES - named arguments, bad callback type} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    function_name='f1',
                    callback='bad',
                    description='some desc'
                }
            }
        } e
        set _ $e
    } {*callback argument given to redict.register_function must be a function*}

    test {LIBRARIES - named arguments, bad description} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    function_name='f1',
                    callback=function()
                        return 'hello'
                    end,
                    description=function() return 1 end
                }
            }
        } e
        set _ $e
    } {*description argument given to redict.register_function must be a string*}

    test {LIBRARIES - named arguments, unknown argument} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    function_name='f1',
                    callback=function()
                        return 'hello'
                    end,
                    description='desc',
                    some_unknown='unknown'
                }
            }
        } e
        set _ $e
    } {*unknown argument given to redict.register_function*}

    test {LIBRARIES - named arguments, missing function name} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    callback=function()
                        return 'hello'
                    end,
                    description='desc'
                }
            }
        } e
        set _ $e
    } {*redict.register_function must get a function name argument*}

    test {LIBRARIES - named arguments, missing callback} {
        catch {
            r function load replace {#!lua name=lib
                redict.register_function{
                    function_name='f1',
                    description='desc'
                }
            }
        } e
        set _ $e
    } {*redict.register_function must get a callback argument*}

    test {FUNCTION - test function restore with function name collision} {
        r function flush
        r function load {#!lua name=lib1
            local function add1(a)
                return a + 1
            end
            redict.register_function(
                'f1',
                function(keys, args)
                    return add1(1)
                end
            )
            redict.register_function(
                'f2',
                function(keys, args)
                    return add1(2)
                end
            )
            redict.register_function(
                'f3',
                function(keys, args)
                    return add1(3)
                end
            )
        }
        set e [r function dump]
        r function flush

        # load a library with different name but with the same function name
        r function load {#!lua name=lib1
            redict.register_function(
                'f6',
                function(keys, args)
                    return 7
                end
            )
        }
        r function load {#!lua name=lib2
            local function add1(a)
                return a + 1
            end
            redict.register_function(
                'f4',
                function(keys, args)
                    return add1(4)
                end
            )
            redict.register_function(
                'f5',
                function(keys, args)
                    return add1(5)
                end
            )
            redict.register_function(
                'f3',
                function(keys, args)
                    return add1(3)
                end
            )
        }

        catch {r function restore $e} error
        assert_match {*Library lib1 already exists*} $error
        assert_equal [r fcall f3 0] {4}
        assert_equal [r fcall f4 0] {5}
        assert_equal [r fcall f5 0] {6}
        assert_equal [r fcall f6 0] {7}

        catch {r function restore $e replace} error
        assert_match {*Function f3 already exists*} $error
        assert_equal [r fcall f3 0] {4}
        assert_equal [r fcall f4 0] {5}
        assert_equal [r fcall f5 0] {6}
        assert_equal [r fcall f6 0] {7}
    }

    test {FUNCTION - test function list with code} {
        r function flush
        r function load {#!lua name=library1
            redict.register_function('f6', function(keys, args) return 7 end)
        }
        r function list withcode
    } {{library_name library1 engine LUA functions {{name f6 description {} flags {}}} library_code {*redict.register_function('f6', function(keys, args) return 7 end)*}}}

    test {FUNCTION - test function list with pattern} {
        r function load {#!lua name=lib1
            redict.register_function('f7', function(keys, args) return 7 end)
        }
        r function list libraryname library*
    } {{library_name library1 engine LUA functions {{name f6 description {} flags {}}}}}

    test {FUNCTION - test function list wrong argument} {
        catch {r function list bad_argument} e
        set _ $e
    } {*Unknown argument bad_argument*}

    test {FUNCTION - test function list with bad argument to library name} {
        catch {r function list libraryname} e
        set _ $e
    } {*library name argument was not given*}

    test {FUNCTION - test function list withcode multiple times} {
        catch {r function list withcode withcode} e
        set _ $e
    } {*Unknown argument withcode*}

    test {FUNCTION - test function list libraryname multiple times} {
        catch {r function list withcode libraryname foo libraryname foo} e
        set _ $e
    } {*Unknown argument libraryname*}

    test {FUNCTION - verify OOM on function load and function restore} {
        r function flush
        r function load replace {#!lua name=test
            redict.register_function('f1', function() return 1 end)
        }
        set payload [r function dump]
        r config set maxmemory 1

        r function flush
        catch {r function load replace {#!lua name=test
            redict.register_function('f1', function() return 1 end)
        }} e
        assert_match {*command not allowed when used memory*} $e

        r function flush
        catch {r function restore $payload} e
        assert_match {*command not allowed when used memory*} $e

        r config set maxmemory 0
    } {OK} {needs:config-maxmemory}

    test {FUNCTION - verify allow-omm allows running any command} {
        r FUNCTION load replace {#!lua name=f1
            redict.register_function{
                function_name='f1',
                callback=function() return redict.call('set', 'x', '1') end,
                flags={'allow-oom'}
            }
        }

        r config set maxmemory 1

        assert_match {OK} [r fcall f1 1 x]
        assert_match {1} [r get x]

        r config set maxmemory 0
    } {OK} {needs:config-maxmemory}
}

start_server {tags {"scripting"}} {
    test {FUNCTION - wrong flags type named arguments} {
        catch {r function load replace {#!lua name=test
            redict.register_function{
                function_name = 'f1',
                callback = function() return 1 end,
                flags = 'bad flags type'
            }
        }} e
        set _ $e
    } {*flags argument to redict.register_function must be a table representing function flags*}

    test {FUNCTION - wrong flag type} {
        catch {r function load replace {#!lua name=test
            redict.register_function{
                function_name = 'f1',
                callback = function() return 1 end,
                flags = {function() return 1 end}
            }
        }} e
        set _ $e
    } {*unknown flag given*}

    test {FUNCTION - unknown flag} {
        catch {r function load replace {#!lua name=test
            redict.register_function{
                function_name = 'f1',
                callback = function() return 1 end,
                flags = {'unknown'}
            }
        }} e
        set _ $e
    } {*unknown flag given*}

    test {FUNCTION - write script on fcall_ro} {
        r function load replace {#!lua name=test
            redict.register_function{
                function_name = 'f1',
                callback = function() return redict.call('set', 'x', 1) end
            }
        }
        catch {r fcall_ro f1 1 x} e
        set _ $e
    } {*Can not execute a script with write flag using \*_ro command*}

    test {FUNCTION - write script with no-writes flag} {
        r function load replace {#!lua name=test
            redict.register_function{
                function_name = 'f1',
                callback = function() return redict.call('set', 'x', 1) end,
                flags = {'no-writes'}
            }
        }
        catch {r fcall f1 1 x} e
        set _ $e
    } {*Write commands are not allowed from read-only scripts*}

    test {FUNCTION - deny oom} {
        r FUNCTION load replace {#!lua name=test
            redict.register_function('f1', function() return redict.call('set', 'x', '1') end)
        }

        r config set maxmemory 1

        catch {[r fcall f1 1 x]} e
        assert_match {OOM *when used memory > 'maxmemory'*} $e

        r config set maxmemory 0
    } {OK} {needs:config-maxmemory}

    test {FUNCTION - deny oom on no-writes function} {
        r FUNCTION load replace {#!lua name=test
            redict.register_function{function_name='f1', callback=function() return 'hello' end, flags={'no-writes'}}
        }

        r config set maxmemory 1

        assert_equal [r fcall f1 1 k] hello
        assert_equal [r fcall_ro f1 1 k] hello

        r config set maxmemory 0
    } {OK} {needs:config-maxmemory}

    test {FUNCTION - allow stale} {
        r FUNCTION load replace {#!lua name=test
            redict.register_function{function_name='f1', callback=function() return 'hello' end, flags={'no-writes'}}
            redict.register_function{function_name='f2', callback=function() return 'hello' end, flags={'allow-stale', 'no-writes'}}
            redict.register_function{function_name='f3', callback=function() return redict.call('get', 'x') end, flags={'allow-stale', 'no-writes'}}
            redict.register_function{function_name='f4', callback=function() return redict.call('info', 'server') end, flags={'allow-stale', 'no-writes'}}
        }

        r config set replica-serve-stale-data no
        r replicaof 127.0.0.1 1

        catch {[r fcall f1 0]} e
        assert_match {MASTERDOWN *} $e

        assert_equal {hello} [r fcall f2 0]

        catch {[r fcall f3 1 x]} e
        assert_match {ERR *Can not execute the command on a stale replica*} $e

        assert_match {*redict_version*} [r fcall f4 0]

        r replicaof no one
        r config set replica-serve-stale-data yes
        set _ {}
    } {} {external:skip}

    test {FUNCTION - redict version api} {
        r FUNCTION load replace {#!lua name=test
            local version = redict.REDICT_VERSION_NUM

            redict.register_function{function_name='get_version_v1', callback=function()
              return string.format('%s.%s.%s',
                                    bit.band(bit.rshift(version, 16), 0x000000ff),
                                    bit.band(bit.rshift(version, 8), 0x000000ff),
                                    bit.band(version, 0x000000ff))
            end}
            redict.register_function{function_name='get_version_v2', callback=function() return redict.REDICT_VERSION end}
        }

        catch {[r fcall f1 0]} e
        set v1 [r fcall get_version_v1 0]
        set v2 [r fcall get_version_v2 0]
        # X.Y.Z-rQ matches X.Y.Z
        assert {$v1 eq $v2 || [string match "$v1-r*" $v2]}
    }

    test {FUNCTION - legacy redis version is hard-coded to 7.2.4} {
        r FUNCTION load replace {#!lua name=test
            local version = redis.REDIS_VERSION_NUM

            redict.register_function{function_name='get_version_v1', callback=function()
              return string.format('%s.%s.%s',
                                    bit.band(bit.rshift(version, 16), 0x000000ff),
                                    bit.band(bit.rshift(version, 8), 0x000000ff),
                                    bit.band(version, 0x000000ff))
            end}
            redis.register_function{function_name='get_version_v2', callback=function() return redis.REDIS_VERSION end}
        }

        catch {[r fcall f1 0]} e
        set v1 [r fcall get_version_v1 0]
        set v2 [r fcall get_version_v2 0]
        # Permanently hard-coded to 7.2.4
        assert {$v1 eq "7.2.4" && $v1 eq $v2}
    }

    test {FUNCTION - function stats} {
        r FUNCTION FLUSH

        r FUNCTION load {#!lua name=test1
            redict.register_function('f1', function() return 1 end)
            redict.register_function('f2', function() return 1 end)
        }

        r FUNCTION load {#!lua name=test2
            redict.register_function('f3', function() return 1 end)
        }

        r function stats
    } {running_script {} engines {LUA {libraries_count 2 functions_count 3}}}

    test {FUNCTION - function stats reloaded correctly from rdb} {
        r debug reload
        r function stats
    } {running_script {} engines {LUA {libraries_count 2 functions_count 3}}} {needs:debug}

    test {FUNCTION - function stats delete library} {
        r function delete test1
        r function stats
    } {running_script {} engines {LUA {libraries_count 1 functions_count 1}}}

    test {FUNCTION - test function stats on loading failure} {
        r FUNCTION FLUSH

        r FUNCTION load {#!lua name=test1
            redict.register_function('f1', function() return 1 end)
            redict.register_function('f2', function() return 1 end)
        }

        catch {r FUNCTION load {#!lua name=test1
            redict.register_function('f3', function() return 1 end)
        }} e
        assert_match "*Library 'test1' already exists*" $e


        r function stats
    } {running_script {} engines {LUA {libraries_count 1 functions_count 2}}}

    test {FUNCTION - function stats cleaned after flush} {
        r function flush
        r function stats
    } {running_script {} engines {LUA {libraries_count 0 functions_count 0}}}

    test {FUNCTION - function test empty engine} {
         catch {r function load replace {#! name=test
            redict.register_function('foo', function() return 1 end)
        }} e
        set _ $e
    } {ERR Engine '' not found}

    test {FUNCTION - function test unknown metadata value} {
         catch {r function load replace {#!lua name=test foo=bar
            redict.register_function('foo', function() return 1 end)
        }} e
        set _ $e
    } {ERR Invalid metadata value given: foo=bar}

    test {FUNCTION - function test no name} {
         catch {r function load replace {#!lua
            redict.register_function('foo', function() return 1 end)
        }} e
        set _ $e
    } {ERR Library name was not given}

    test {FUNCTION - function test multiple names} {
         catch {r function load replace {#!lua name=foo name=bar
            redict.register_function('foo', function() return 1 end)
        }} e
        set _ $e
    } {ERR Invalid metadata value, name argument was given multiple times}

    test {FUNCTION - function test name with quotes} {
        r function load replace {#!lua name="foo"
            redict.register_function('foo', function() return 1 end)
        }
    } {foo}

    test {FUNCTION - trick global protection 1} {
        r FUNCTION FLUSH

        r FUNCTION load {#!lua name=test1
            redict.register_function('f1', function()
                mt = getmetatable(_G)
                original_globals = mt.__index
                original_globals['redict'] = function() return 1 end
            end)
        }

        catch {[r fcall f1 0]} e
        set _ $e
    } {*Attempt to modify a readonly table*}

    test {FUNCTION - test getmetatable on script load} {
        r FUNCTION FLUSH

        catch {
            r FUNCTION load {#!lua name=test1
                mt = getmetatable(_G)
            }
        } e

        set _ $e
    } {*Script attempted to access nonexistent global variable 'getmetatable'*}

}
