# vim:set ft= ts=4 sw=4 et fdm=marker:

use Test::Nginx::Socket::Lua;
use Cwd qw(cwd);

repeat_each(2);

plan tests => repeat_each() * (blocks() * 4);

#no_diff();
#no_long_string();

my $pwd = cwd();

our $HttpConfig = <<_EOC_;
    lua_package_path "$pwd/../lua-resty-core/lib/?.lua;../lua-resty-lrucache/lib/?.lua;$pwd/lib/?.lua;;";
    init_by_lua_block {
        local v = require "jit.v"
        -- v.on("/tmp/a.dump")
        require "resty.core"
    }
_EOC_

no_long_string();
run_tests();

__DATA__

=== TEST 1: sanity (output states)
--- http_config eval
"
$::HttpConfig

    lua_shared_dict req 1m;
    lua_shared_dict conn 1m;
    lua_shared_dict count 1m;
"
--- config
    location = /t {
        content_by_lua_block {
            local limit_conn = require "resty.limit.conn"
            local limit_req = require "resty.limit.req"
            local limit_count = require "resty.limit.count"
            local limit_traffic = require "resty.limit.traffic"

            local lim1 = limit_req.new("req", 3, 2)
            local lim2 = limit_req.new("req", 2, 3)
            local lim3 = limit_conn.new("conn", 4, 1, 2)
            local lim4 = limit_count.new("count", 10, 100)

            local limiters = {lim1, lim2, lim3, lim4}

            ngx.shared.req:flush_all()
            ngx.shared.conn:flush_all()
            ngx.shared.count:flush_all()

            local keys = {"foo", "bar", "foo", "bar"}
            local states = {}
            for i = 1, 6 do
                local delay, err = limit_traffic.combine(limiters, keys, states)
                if not delay then
                    ngx.say("failed to limit traffic: ", err)
                else
                    ngx.say(i, ": ", delay,
                            ", conn committed: ", lim3:is_committed(),
                            ", states: ", table.concat(states, ", "))
                end
                if i == 4 then
                    ngx.sleep(1)
                end
            end
        }
    }
--- request
    GET /t
--- response_body_like eval
qr/^1: 0, conn committed: true, states: 0, 0, 1, 9
2: 0\.5, conn committed: true, states: 1, 1, 2, 8
3: 1, conn committed: true, states: 2, 2, 3, 7
failed to limit traffic: rejected
5: 0\.(?:4[6-9]|5|5[0-4])\d*, conn committed: true, states: 0(?:\.0\d+)?, (?:1|1\.0[0-4]\d*|0\.9[6-9]\d*), 4, 6
6: 2, conn committed: true, states: 1(?:\.0\d+)?, (?:2|2\.0[0-4]\d*|1\.9[6-9]\d*), 5, 5
$/s
--- no_error_log
[error]
[lua]



=== TEST 2: sanity (no output states)
--- http_config eval
"
$::HttpConfig

    lua_shared_dict req 1m;
    lua_shared_dict conn 1m;
    lua_shared_dict count 1m;
"
--- config
    location = /t {
        content_by_lua_block {
            local limit_conn = require "resty.limit.conn"
            local limit_req = require "resty.limit.req"
            local limit_count = require "resty.limit.count"
            local limit_traffic = require "resty.limit.traffic"

            local lim1 = limit_req.new("req", 3, 2)
            local lim2 = limit_req.new("req", 2, 3)
            local lim3 = limit_conn.new("conn", 4, 1, 2)
            local lim4 = limit_count.new("count", 10, 100)

            local limiters = {lim1, lim2, lim3, lim4}

            ngx.shared.req:flush_all()
            ngx.shared.conn:flush_all()
            ngx.shared.count:flush_all()

            local keys = {"foo", "bar", "foo", "bar"}
            for i = 1, 6 do
                local delay, err = limit_traffic.combine(limiters, keys)
                if not delay then
                    ngx.say("failed to limit traffic: ", err)
                else
                    ngx.say(i, ": ", delay,
                            ", conn committed: ", lim3:is_committed())
                end
                if i == 4 then
                    ngx.sleep(1)
                end
            end
        }
    }
--- request
    GET /t
--- response_body_like eval
qr/^1: 0, conn committed: true
2: 0\.5, conn committed: true
3: 1, conn committed: true
failed to limit traffic: rejected
5: 0\.(?:4[6-9]|5|5[0-4])\d*, conn committed: true
6: 2, conn committed: true
$/s
--- no_error_log
[error]
[lua]



=== TEST 3: block by limit-count (output states)
--- http_config eval
"
$::HttpConfig

    lua_shared_dict req 1m;
    lua_shared_dict conn 1m;
    lua_shared_dict count 1m;
"
--- config
    location = /t {
        content_by_lua_block {
            local limit_conn = require "resty.limit.conn"
            local limit_req = require "resty.limit.req"
            local limit_count = require "resty.limit.count"
            local limit_traffic = require "resty.limit.traffic"

            local lim1 = limit_req.new("req", 3, 2)
            local lim2 = limit_req.new("req", 2, 3)
            local lim3 = limit_conn.new("conn", 4, 1, 2)
            local lim4 = limit_count.new("count", 2, 100)

            local limiters = {lim1, lim2, lim3, lim4}

            ngx.shared.req:flush_all()
            ngx.shared.conn:flush_all()
            ngx.shared.count:flush_all()

            local keys = {"foo", "bar", "foo", "bar"}
            local states = {}
            for i = 1, 6 do
                local delay, err = limit_traffic.combine(limiters, keys, states)
                if not delay then
                    ngx.say("failed to limit traffic: ", err)
                    ngx.say("states: ", table.concat(states, ", "))
                else
                    ngx.say(i, ": ", delay,
                            ", conn committed: ", lim3:is_committed(),
                            ", states: ", table.concat(states, ", "))
                end
            end
        }
    }
--- request
    GET /t
--- response_body_like eval
qr/^1: 0, conn committed: true, states: 0, 0, 1, 1
2: 0\.5, conn committed: true, states: 1, 1, 2, 0
failed to limit traffic: rejected
states: 1, 1, 2, 0
failed to limit traffic: rejected
states: 1, 1, 2, 0
failed to limit traffic: rejected
states: 1, 1, 2, 0
failed to limit traffic: rejected
states: 1, 1, 2, 0
$/s
--- no_error_log
[error]
[lua]



=== TEST 4: sanity (uncommit() previous limiters if a limiter rejects while committing a state)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local limit_traffic = require "resty.limit.traffic"

            local limit_mock = {}
            limit_mock.__index = limit_mock

            function limit_mock.new(_, _, reject_on_commit)
                return setmetatable({
                    counters = {},
                    reject_on_commit = reject_on_commit,
                }, limit_mock)
            end

            function limit_mock:incoming(key, commit)
                local count = self.counters[key] or 0

                count = count + 1

                if commit then
                    self.counters[key] = count

                    if self.reject_on_commit then
                        return nil, "rejected by mock limiter"
                    end
                end

                return count
            end

            function limit_mock:uncommit(key)
                local count = self.counters[key] or 0
                if count > 0 then
                    count = count - 1
                end

                self.counters[key] = count
            end

            local lim1 = limit_mock.new(nil, 2)
            local lim2 = limit_mock.new(nil, 2)
            local lim3 = limit_mock.new(nil, 2, true)
            local lim4 = limit_mock.new(nil, 2)

            local limiters = {lim1, lim2, lim3, lim4}

            local keys = {"foo", "bar", "baz", "bat"}

            local delay, err = limit_traffic.combine(limiters, keys)
            if not delay then
                ngx.say(err)
            end

            ngx.say("state lim1: ", lim1:incoming(keys[1])) -- should be 1 because previous combine() call was uncommitted
            ngx.say("state lim2: ", lim2:incoming(keys[2])) -- should be 1 because previous combine() call was uncommitted
            ngx.say("state lim3: ", lim3:incoming(keys[3]))
        }
    }
--- request
    GET /t
--- response_body
rejected by mock limiter
state lim1: 1
state lim2: 1
state lim3: 2
--- no_error_log
[error]
[lua]



=== TEST 5: sanity (uncommit() the previous limiters and the last limiter if a limiter rejects while committing a state)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local limit_traffic = require "resty.limit.traffic"

            local limit_mock = {}
            limit_mock.__index = limit_mock

            function limit_mock.new(_, _, reject_on_commit)
                return setmetatable({
                    counters = {},
                    reject_on_commit = reject_on_commit,
                }, limit_mock)
            end

            function limit_mock:incoming(key, commit)
                local count = self.counters[key] or 0

                count = count + 1

                if commit then
                    self.counters[key] = count

                    if self.reject_on_commit then
                        return nil, "rejected by mock limiter"
                    end
                end

                return count
            end

            function limit_mock:uncommit(key)
                local count = self.counters[key] or 0
                if count > 0 then
                    count = count - 1
                end

                self.counters[key] = count
            end

            local lim1 = limit_mock.new(nil, 2)
            local lim2 = limit_mock.new(nil, 2, true)
            local lim3 = limit_mock.new(nil, 2)
            local lim4 = limit_mock.new(nil, 2)

            local limiters = {lim1, lim2, lim3, lim4}

            local keys = {"foo", "bar", "baz", "bat"}

            local delay, err = limit_traffic.combine(limiters, keys)
            if not delay then
                ngx.say(err)
            end

            ngx.say("state lim1: ", lim1:incoming(keys[1])) -- should be 1 because previous combine() call was uncommitted
            ngx.say("state lim2: ", lim2:incoming(keys[2]))
            ngx.say("state lim3: ", lim3:incoming(keys[3])) -- should be 1 because previous combine() call was uncommitted
            ngx.say("state lim4: ", lim4:incoming(keys[4])) -- should be 1 because previous combine() call was uncommitted
        }
    }
--- request
    GET /t
--- response_body
rejected by mock limiter
state lim1: 1
state lim2: 2
state lim3: 1
state lim4: 1
--- no_error_log
[error]
[lua]
