#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX;

my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
my $version = eval { `$nginx_binary -V 2>&1` };

if ($version !~ m/\/apisix-nginx-module/) {
    plan(skip_all => "apisix-nginx-module not installed");
} else {
    plan('no_plan');
}

$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;

add_block_preprocessor(sub {
    my ($block) = @_;

    if (!$block->extra_yaml_config) {
        my $extra_yaml_config = <<_EOC_;
xrpc:
  protocols:
    - name: redis
_EOC_
        $block->set_value("extra_yaml_config", $extra_yaml_config);
    }

    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
        $block->set_value("no_error_log", "[error]\nRPC is not finished");
    }

    if (!defined $block->request) {
        $block->set_value("request", "GET /t");
    }

    $block;
});

worker_connections(1024);
run_tests;

__DATA__

=== TEST 1: init
--- config
    location /t {
        content_by_lua_block {
            local t = require("lib.test_admin").test
            local code, body = t('/apisix/admin/stream_routes/1',
                ngx.HTTP_PUT,
                {
                    protocol = {
                        name = "redis"
                    },
                    upstream = {
                        nodes = {
                            ["127.0.0.1:6379"] = 1
                        },
                        type = "roundrobin"
                    }
                }
            )
            if code >= 300 then
                ngx.status = code
            end
            ngx.say(body)
        }
    }
--- response_body
passed



=== TEST 2: sanity
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local res, err = red:hmset("animals", "dog", "bark", "cat", "meow")
            if not res then
                ngx.say("failed to set animals: ", err)
                return
            end
            ngx.say("hmset animals: ", res)

            local res, err = red:hmget("animals", "dog", "cat")
            if not res then
                ngx.say("failed to get animals: ", err)
                return
            end

            ngx.say("hmget animals: ", res)

            local res, err = red:hget("animals", "dog")
            if not res then
                ngx.say("failed to get animals: ", err)
                return
            end

            ngx.say("hget animals: ", res)

            local res, err = red:hget("animals", "not_found")
            if not res then
                ngx.say("failed to get animals: ", err)
                return
            end

            ngx.say("hget animals: ", res)
        }
    }
--- response_body
hmset animals: OK
hmget animals: barkmeow
hget animals: bark
hget animals: null
--- stream_conf_enable



=== TEST 3: error
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local res, err = red:get("animals")
            if not res then
                ngx.say("failed to set animals: ", err)
            end

            local res, err = red:hget("animals", "dog")
            if not res then
                ngx.say("failed to get animals: ", err)
                return
            end

            ngx.say("hget animals: ", res)
        }
    }
--- response_body
failed to set animals: WRONGTYPE Operation against a key holding the wrong kind of value
hget animals: bark
--- stream_conf_enable



=== TEST 4: big value
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local res, err = red:set("big-key", ("\r\n"):rep(1024 * 1024 * 16))
            if not res then
                ngx.say("failed to set: ", err)
                return
            end

            local res, err = red:get("big-key")
            if not res then
                ngx.say("failed to get: ", err)
                return
            end

            ngx.print(res)
        }
    }
--- response_body eval
"\r\n" x 16777216
--- stream_conf_enable



=== TEST 5: pipeline
--- config
    location /t {
        content_by_lua_block {
            local cjson = require("cjson")
            local redis = require "resty.redis"

            local t = {}
            for i = 1, 180 do
                local th = assert(ngx.thread.spawn(function(i)
                    local red = redis:new()
                    local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
                    if not ok then
                        ngx.say("failed to connect: ", err)
                        return
                    end

                    red:init_pipeline()

                    red:set("mark_" .. i, i)
                    red:get("mark_" .. i)
                    red:get("counter")
                    for j = 1, 4 do
                        red:incr("counter")
                    end

                    local results, err = red:commit_pipeline()
                    if not results then
                        ngx.say("failed to commit: ", err)
                        return
                    end

                    local begin = tonumber(results[3])
                    for j = 1, 4 do
                        local incred = results[3 + j]
                        if incred ~= results[2 + j] + 1 then
                            ngx.log(ngx.ERR, cjson.encode(results))
                        end
                    end
                end, i))
                table.insert(t, th)
            end
            for i, th in ipairs(t) do
                ngx.thread.wait(th)
            end
        }
    }
--- response_body
--- stream_conf_enable



=== TEST 6: delay
--- config
    location /t {
        content_by_lua_block {
            local t = require("lib.test_admin").test
            local code, body = t('/apisix/admin/stream_routes/1',
                ngx.HTTP_PUT,
                {
                    protocol = {
                        name = "redis",
                        conf = {
                            faults = {
                                {delay = 0.01, key = "ignored", commands = {"Ping", "time"}}
                            }
                        }
                    },
                    upstream = {
                        nodes = {
                            ["127.0.0.1:6379"] = 1
                        },
                        type = "roundrobin"
                    }
                }
            )
            if code >= 300 then
                ngx.status = code
            end
            ngx.say(body)
        }
    }
--- response_body
passed



=== TEST 7: hit
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local start = ngx.now()
            local res, err = red:ping()
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            -- use integer to bypass float point number precision problem
            if math.ceil((now - start) * 1000) < 10 then
                ngx.say(now, " ", start)
                return
            end
            start = now

            local res, err = red:time()
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 10 then
                ngx.say(now, " ", start)
                return
            end
            start = now

            red:init_pipeline()
            red:time()
            red:time()
            red:get("A")

            local results, err = red:commit_pipeline()
            if not results then
                ngx.say("failed to commit: ", err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then
                ngx.say(now, " ", start)
                return
            end

            ngx.say("ok")
        }
    }
--- response_body
ok
--- stream_conf_enable



=== TEST 8: DFS match
--- config
    location /t {
        content_by_lua_block {
            local t = require("lib.test_admin").test
            local code, body = t('/apisix/admin/stream_routes/1',
                ngx.HTTP_PUT,
                {
                    protocol = {
                        name = "redis",
                        conf = {
                            faults = {
                                {delay = 0.02, key = "a", commands = {"get"}},
                                {delay = 0.01, commands = {"get", "set"}},
                            }
                        }
                    },
                    upstream = {
                        nodes = {
                            ["127.0.0.1:6379"] = 1
                        },
                        type = "roundrobin"
                    }
                }
            )
            if code >= 300 then
                ngx.status = code
            end
            ngx.say(body)
        }
    }
--- response_body
passed



=== TEST 9: hit
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local start = ngx.now()
            local res, err = red:get("a")
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 20 then
                ngx.say(now, " ", start)
                return
            end
            start = now

            local res, err = red:set("a", "a")
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 10 then
                ngx.say(now, " ", start)
                return
            end
            start = now

            red:init_pipeline()
            red:get("b")
            red:set("A", "a")

            local results, err = red:commit_pipeline()
            if not results then
                ngx.say("failed to commit: ", err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then
                ngx.say(now, " ", start)
                return
            end

            ngx.say("ok")
        }
    }
--- response_body
ok
--- stream_conf_enable



=== TEST 10: multi keys
--- config
    location /t {
        content_by_lua_block {
            local t = require("lib.test_admin").test
            local code, body = t('/apisix/admin/stream_routes/1',
                ngx.HTTP_PUT,
                {
                    protocol = {
                        name = "redis",
                        conf = {
                            faults = {
                                {delay = 0.06, key = "b", commands = {"del"}},
                                {delay = 0.04, key = "a", commands = {"mset"}},
                                {delay = 0.02, key = "b", commands = {"mset"}},
                            }
                        }
                    },
                    upstream = {
                        nodes = {
                            ["127.0.0.1:6379"] = 1
                        },
                        type = "roundrobin"
                    }
                }
            )
            if code >= 300 then
                ngx.status = code
            end
            ngx.say(body)
        }
    }
--- response_body
passed



=== TEST 11: hit
--- config
    location /t {
        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local start = ngx.now()
            local res, err = red:mset("c", 1, "a", 2, "b", 3)
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 40 then
                ngx.say("mset a ", now, " ", start)
                return
            end
            start = now

            local res, err = red:mset("b", 2, "a", 3)
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 35 then
                ngx.say("mset b ", now, " ", start)
                return
            end
            start = now

            local res, err = red:mset("c", "a")
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) > 20 then
                ngx.say("mset mismatch ", now, " ", start)
                return
            end
            start = now

            local res, err = red:del("a", "b")
            if not res then
                ngx.say(err)
                return
            end
            local now = ngx.now()
            if math.ceil((now - start) * 1000) < 60 then
                ngx.say("del b ", now, " ", start)
                return
            end
            start = now

            ngx.say("ok")
        }
    }
--- response_body
ok
--- stream_conf_enable



=== TEST 12: publish & subscribe
--- stream_extra_init_by_lua
            local cjson = require "cjson"
            local redis_proto = require("apisix.stream.xrpc.protocols.redis")
            redis_proto.log = function(sess, ctx)
                ngx.log(ngx.WARN, "log redis request ", cjson.encode(ctx.cmd_line))
            end

--- config
    location /t {
        content_by_lua_block {
            local cjson = require "cjson"
            local redis = require "resty.redis"

            local red = redis:new()
            local red2 = redis:new()

            red:set_timeout(1000) -- 1 sec
            red2:set_timeout(1000) -- 1 sec

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("1: failed to connect: ", err)
                return
            end

            ok, err = red2:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("2: failed to connect: ", err)
                return
            end

            local res, err = red:subscribe("dog")
            if not res then
                ngx.say("1: failed to subscribe: ", err)
                return
            end

            ngx.say("1: subscribe dog: ", cjson.encode(res))

            res, err = red:subscribe("cat")
            if not res then
                ngx.say("1: failed to subscribe: ", err)
                return
            end

            ngx.say("1: subscribe cat: ", cjson.encode(res))

            res, err = red2:publish("dog", "Hello")
            if not res then
                ngx.say("2: failed to publish: ", err)
                return
            end

            ngx.say("2: publish: ", cjson.encode(res))

            res, err = red:read_reply()
            if not res then
                ngx.say("1: failed to read reply: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end

            red:set_timeout(10) -- 10ms
            res, err = red:read_reply()
            if not res then
                ngx.say("1: failed to read reply: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end
            red:set_timeout(1000) -- 1s

            res, err = red:unsubscribe()
            if not res then
                ngx.say("1: failed to unscribe: ", err)
            else
                ngx.say("1: unsubscribe: ", cjson.encode(res))
            end

            res, err = red:read_reply()
            if not res then
                ngx.say("1: failed to read reply: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end

            red:set_timeout(10) -- 10ms
            res, err = red:read_reply()
            if not res then
                ngx.say("1: failed to read reply: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end
            red:set_timeout(1000) -- 1s

            res, err = red:set("dog", 1)
            if not res then
                ngx.say("1: failed to set: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end

            red:close()
            red2:close()
        }
    }
--- response_body_like chop
^1: subscribe dog: \["subscribe","dog",1\]
1: subscribe cat: \["subscribe","cat",2\]
2: publish: 1
1: receive: \["message","dog","Hello"\]
1: failed to read reply: timeout
1: unsubscribe: \[\["unsubscribe","(?:dog|cat)",1\],\["unsubscribe","(?:dog|cat)",0\]\]
1: failed to read reply: not subscribed
1: failed to read reply: not subscribed
1: receive: "OK"
$
--- stream_conf_enable
--- grep_error_log eval
qr/log redis request \[[^]]+\]/
--- grep_error_log_out
log redis request ["subscribe","dog"]
log redis request ["subscribe","cat"]
log redis request ["publish","dog","Hello"]
log redis request ["unsubscribe"]
log redis request ["set","dog","1"]



=== TEST 13: psubscribe & punsubscribe
--- stream_extra_init_by_lua
            local cjson = require "cjson"
            local redis_proto = require("apisix.stream.xrpc.protocols.redis")
            redis_proto.log = function(sess, ctx)
                ngx.log(ngx.WARN, "log redis request ", cjson.encode(ctx.cmd_line))
            end

--- config
    location /t {
        content_by_lua_block {
            local cjson = require "cjson"
            local redis = require "resty.redis"

            local red = redis:new()
            local red2 = redis:new()

            red:set_timeout(1000) -- 1 sec
            red2:set_timeout(1000) -- 1 sec

            local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("1: failed to connect: ", err)
                return
            end

            ok, err = red2:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
            if not ok then
                ngx.say("2: failed to connect: ", err)
                return
            end

            local res, err = red:psubscribe("dog*", "cat*")
            if not res then
                ngx.say("1: failed to subscribe: ", err)
                return
            end

            ngx.say("1: psubscribe: ", cjson.encode(res))

            res, err = red2:publish("dog1", "Hello")
            if not res then
                ngx.say("2: failed to publish: ", err)
                return
            end

            ngx.say("2: publish: ", cjson.encode(res))

            res, err = red:read_reply()
            if not res then
                ngx.say("1: failed to read reply: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end

            res, err = red:punsubscribe("cat*", "dog*")
            if not res then
                ngx.say("1: failed to unscribe: ", err)
            else
                ngx.say("1: punsubscribe: ", cjson.encode(res))
            end

            res, err = red:set("dog", 1)
            if not res then
                ngx.say("1: failed to set: ", err)
            else
                ngx.say("1: receive: ", cjson.encode(res))
            end

            red:close()
            red2:close()
        }
    }
--- response_body_like chop
^1: psubscribe: \[\["psubscribe","dog\*",1\],\["psubscribe","cat\*",2\]\]
2: publish: 1
1: receive: \["pmessage","dog\*","dog1","Hello"\]
1: punsubscribe: \[\["punsubscribe","cat\*",1\],\["punsubscribe","dog\*",0\]\]
1: receive: "OK"
$
--- stream_conf_enable
--- grep_error_log eval
qr/log redis request \[[^]]+\]/
--- grep_error_log_out
log redis request ["psubscribe","dog*","cat*"]
log redis request ["publish","dog1","Hello"]
log redis request ["punsubscribe","cat*","dog*"]
log redis request ["set","dog","1"]
