local helpers           = require "spec.helpers"
local cjson             = require "cjson"
local redis_helper      = require "spec.helpers.redis_helper"


local REDIS_HOST        = helpers.redis_host
local REDIS_PORT        = helpers.redis_port
local REDIS_SSL_PORT    = helpers.redis_ssl_port
local REDIS_SSL_SNI     = helpers.redis_ssl_sni
local REDIS_PASSWORD    = ""
local REDIS_DATABASE    = 1
local UPSTREAM_HOST     = "localhost"

local fmt               = string.format
local proxy_client      = helpers.proxy_client
local table_insert      = table.insert
local tonumber          = tonumber

local ngx_sleep         = ngx.sleep
local ngx_now           = ngx.now


-- This performs the test up to two times (and no more than two).
-- We are **not** retrying to "give it another shot" in case of a flaky test.
-- The reason why we allow for a single retry in this test suite is because
-- tests are dependent on the value of the current minute. If the minute
-- flips during the test (i.e. going from 03:43:59 to 03:44:00), the result
-- will fail. Since each test takes less than a minute to run, running it
-- a second time right after that failure ensures that another flip will
-- not occur. If the second execution failed as well, this means that there
-- was an actual problem detected by the test.
local function retry(fn)
  if not pcall(fn) then
    ngx_sleep(61 - (ngx_now() % 60))  -- Wait for minute to expire
    fn()
  end
end


local function GET(url, opt)
  local client = proxy_client()
  local res, err  = client:get(url, opt)
  if not res then
    client:close()
    return nil, err
  end

  assert(res:read_body())

  client:close()

  return res
end


local function client_requests(n, proxy_fn)
  local ret = {
    minute_limit     = {},
    minute_remaining = {},

    hour_limit       = {},
    hour_remaining   = {},

    limit            = {},
    remaining        = {},

    status           = {},
    reset            = {},
  }

  for _ = 1, n do
    local res = assert(proxy_fn())

    table_insert(ret.reset, tonumber(res.headers["RateLimit-Reset"]))

    table_insert(ret.status, res.status)

    table_insert(ret.minute_limit, tonumber(res.headers["X-RateLimit-Limit-Minute"]))
    table_insert(ret.minute_remaining, tonumber(res.headers["X-RateLimit-Remaining-Minute"]))

    table_insert(ret.hour_limit, tonumber(res.headers["X-RateLimit-Limit-Hour"]))
    table_insert(ret.hour_remaining, tonumber(res.headers["X-RateLimit-Remaining-Hour"]))

    table_insert(ret.limit, tonumber(res.headers["RateLimit-Limit"]))
    table_insert(ret.remaining, tonumber(res.headers["RateLimit-Remaining"]))

    helpers.wait_timer("rate-limiting", true, "any-finish")
  end

  return ret
end


local function validate_headers(headers, check_minute, check_hour)
  if check_minute then
    assert.same({
      6, 6, 6, 6, 6, 6, 6,
    }, headers.minute_limit)

    assert.same({
      5, 4, 3, 2, 1, 0, 0,
    }, headers.minute_remaining)
  end

  if check_hour then
    for _, v in ipairs(headers.hour_limit) do
      assert(v > 0)
    end

    for _, v in ipairs(headers.hour_remaining) do
      assert(v >= 0)
    end
  end

  assert.same({
    6, 6, 6, 6, 6, 6, 6,
  }, headers.limit)

  assert.same({
    5, 4, 3, 2, 1, 0, 0,
  }, headers.remaining)

  assert.same({
    200, 200, 200, 200, 200, 200, 429,
  }, headers.status)

  for _, reset in ipairs(headers.reset) do
    if check_hour then
      assert.equal(true, reset <= 3600 and reset >= 0)

    elseif check_minute then
      assert.equal(true, reset <= 60 and reset >= 0)

    else
      error("check_hour or check_minute must be true")
    end

  end
end


local function setup_service(admin_client, url)
  local service = assert(admin_client:send({
      method = "POST",
      path = "/services",
      body = {
        url = url,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))

  return cjson.decode(assert.res_status(201, service))
end

local function setup_route(admin_client, service, paths, protocol)
  protocol = protocol or "http"
  local route = assert(admin_client:send({
      method = "POST",
      path = "/routes",
      body = {
        protocols = { protocol },
        service = { id = service.id, },
        paths = paths,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))

  return cjson.decode(assert.res_status(201, route))
end

local function setup_rl_plugin(admin_client, conf, service, consumer)
  local plugin

  if service then
    plugin = assert(admin_client:send({
      method = "POST",
      path = "/plugins",
      body = {
        name = "rate-limiting",
        service = { id = service.id, },
        config = conf,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))

  elseif consumer then
    plugin = assert(admin_client:send({
      method = "POST",
      path = "/plugins",
      body = {
        name = "rate-limiting",
        consumer = { id = consumer.id, },
        config = conf,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))

  else
    plugin = assert(admin_client:send({
      method = "POST",
      path = "/plugins",
      body = {
        name = "rate-limiting",
        config = conf,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))
  end

  return cjson.decode(assert.res_status(201, plugin))
end

local function setup_key_auth_plugin(admin_client, conf, service)
  local plugin

  if service then
    plugin = assert(admin_client:send({
      method = "POST",
      path = "/plugins",
      body = {
        name = "key-auth",
        service = { id = service.id, },
        config = conf,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))

  else
    plugin = assert(admin_client:send({
      method = "POST",
      path = "/plugins",
      body = {
        name = "key-auth",
        config = conf,
      },
      headers = {
        ["Content-Type"] = "application/json",
      },
    }))
  end

  return cjson.decode(assert.res_status(201, plugin))
end

local function setup_consumer(admin_client, username)
  local consumer = assert(admin_client:send({
    method = "POST",
    path = "/consumers",
    body = {
      username = username,
    },
    headers = {
      ["Content-Type"] = "application/json",
    },
  }))

  return cjson.decode(assert.res_status(201, consumer))
end

local function setup_credential(admin_client, consumer, key)
  local credential = assert(admin_client:send({
    method = "POST",
    path = "/consumers/" .. consumer.id .. "/key-auth",
    body = {
      key = key,
    },
    headers = {
      ["Content-Type"] = "application/json",
    },
  }))

  return cjson.decode(assert.res_status(201, credential))
end


local function delete_service(admin_client, service)
  local res = assert(admin_client:send({
    method = "DELETE",
    path = "/services/" .. service.id,
  }))

  assert.res_status(204, res)
end

local function delete_route(admin_client, route)
  local res = assert(admin_client:send({
    method = "DELETE",
    path = "/routes/" .. route.id,
  }))

  assert.res_status(204, res)
end

local function delete_plugin(admin_client, plugin)
  local res = assert(admin_client:send({
    method = "DELETE",
    path = "/plugins/" .. plugin.id,
  }))

  assert.res_status(204, res)
end

local function delete_consumer(admin_client, consumer)
  local res = assert(admin_client:send({
    method = "DELETE",
    path = "/consumers/" .. consumer.id,
  }))

  assert.res_status(204, res)
end

local function delete_credential(admin_client, credential)
  local res = assert(admin_client:send({
    method = "DELETE",
    path = "/consumers/" .. credential.consumer.id .. "/key-auth/" .. credential.id,
  }))

  assert.res_status(204, res)
end


local limit_by_confs = {
  "ip",
  "consumer",
  "credential",
  "service",
  "header",
  "path",
}


local ssl_confs = {
  no_ssl = {
    redis_port = REDIS_PORT,
  },
  ssl_verify = {
    redis_ssl = true,
    redis_ssl_verify = true,
    redis_server_name = REDIS_SSL_SNI,
    redis_port = REDIS_SSL_PORT,
  },
  ssl_no_verify = {
    redis_ssl = true,
    redis_ssl_verify = false,
    redis_server_name = "really.really.really.does.not.exist.host.test",
    redis_port = REDIS_SSL_PORT,
  },
}


local desc

for _, strategy in helpers.each_strategy() do
for __, policy in ipairs({ "local", "cluster", "redis" }) do
for ___, limit_by in ipairs(limit_by_confs) do
for ssl_conf_name, ssl_conf in pairs(ssl_confs) do

if ssl_conf_name ~= "no_ssl" and policy ~= "redis" then
  goto continue
end

desc = fmt("Plugin: rate-limiting #db (access) [strategy: %s] [policy: %s] [limit_by: %s] [redis: %s]",
                 strategy, policy, limit_by, ssl_conf_name)

describe(desc, function()
  local db, https_server, admin_client

  local UPSTREAM_PORT
  local UPSTREAM_URL

  lazy_setup(function()
    UPSTREAM_PORT = helpers.get_available_port()
    UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)

    _, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })

    if policy == "redis" then
      redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)

    elseif policy == "cluster" then
      db:truncate("ratelimiting_metrics")
    end

    https_server = helpers.https_server.new(UPSTREAM_PORT)
    https_server:start()

    helpers.start_kong({
      database   = strategy,
      nginx_conf = "spec/fixtures/custom_nginx.template",
      plugins = "bundled,rate-limiting,key-auth",
      trusted_ips = "0.0.0.0/0,::/0",
      lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
    })
  end)


  lazy_teardown(function()
    assert(https_server, "unexpected error")
    https_server:shutdown()
    assert(helpers.stop_kong(), "failed to stop Kong")
  end)

  before_each(function ()
    admin_client = helpers.admin_client()

    if strategy == "cluster" then
      db:truncate("ratelimiting_metrics")
    end

    if policy == "redis" then
      redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
    end
  end)

  after_each(function()
    admin_client:close()
  end)

  it(fmt("blocks if exceeding limit (single %s)", limit_by), function()
    local test_path = "/test"
    local test_header = "test-header"
    local test_key_name = "test-key"
    local test_credential = "test_credential"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = limit_by,
      path                = test_path,                  -- only for limit_by = "path"
      header_name         = test_header,                -- only for limit_by = "header"
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    local auth_plugin
    local consumer
    local credential

    if limit_by == "consumer" or limit_by == "credential" then
      auth_plugin = setup_key_auth_plugin(admin_client, {
        key_names = { test_key_name },
      }, service)
      consumer = setup_consumer(admin_client, "Bob")
      credential = setup_credential(admin_client, consumer, test_credential)
    end

    finally(function()
      if limit_by == "consumer" or limit_by == "credential" then
        delete_credential(admin_client, credential)
        delete_consumer(admin_client, consumer)
        delete_plugin(admin_client, auth_plugin)
      end

      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local function proxy_fn()
      if limit_by == "ip" or
         limit_by == "path" or
         limit_by == "service" then
        return GET(test_path)
      end

      if limit_by == "header" then
        return GET(test_path, { [test_header] = "test" })
      end

      if limit_by == "consumer" or limit_by == "credential" then
        return GET(test_path, { headers = { [test_key_name] = test_credential }})
      end

      error("unexpected limit_by: " .. limit_by)
    end

    retry(function ()
      validate_headers(client_requests(7, proxy_fn), true)
    end)
  end)

if limit_by == "ip" then
  it("blocks if exceeding limit (multiple ip)", function()
    local test_path = "/test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "ip",
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      for _, ip in ipairs({ "127.0.0.1", "127.0.0.2" }) do
        validate_headers(client_requests(7, function ()
          return GET(test_path, { headers = { ["X-Real-IP"] = ip }})
        end), true)
      end     -- for _, ip in ipairs({ "127.0.0.1", "127.0.0.2" }) do
    end)      -- retry(function ()
  end)        -- it("blocks if exceeding limit (multiple ip)", function()

  it("blocks if exceeding limit #grpc (single ip)", function()
    local test_path = "/hello.HelloService/"

    local service = setup_service(admin_client, helpers.grpcbin_url)
    local route = setup_route(admin_client, service, { test_path }, "grpc")
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "ip",
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      for i = 1, 6 do
        local ok, res = helpers.proxy_client_grpc(){
          service = "hello.HelloService.SayHello",
          opts = {
            ["-v"] = true,
          },
        }

        assert.is_true(ok, res)

        assert.matches("x%-ratelimit%-limit%-minute: 6", res)
        assert.matches("x%-ratelimit%-remaining%-minute: " .. (6 - i), res)
        assert.matches("ratelimit%-limit: 6", res)
        assert.matches("ratelimit%-remaining: " .. (6 - i), res)

        local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
        assert.equal(true, reset <= 60 and reset >= 0)

        -- wait for zero-delay timer
        helpers.wait_timer("rate-limiting", true, "any-finish")
      end

      -- Additional request, while limit is 6/minute
      local ok, res = helpers.proxy_client_grpc(){
        service = "hello.HelloService.SayHello",
        opts = {
          ["-v"] = true,
        },
      }
      assert.falsy(ok)
      assert.matches("Code: ResourceExhausted", res)

      assert.matches("ratelimit%-limit: 6", res)
      assert.matches("ratelimit%-remaining: 0", res)

      local retry = tonumber(string.match(res, "retry%-after: (%d+)"))
      assert.equal(true, retry <= 60 and retry > 0)

      local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
      assert.equal(true, reset <= 60 and reset > 0)
    end)
  end)

  it("hide_client_headers (single ip)", function ()
    local test_path = "/test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "ip",
      hide_client_headers = true,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local res = assert(GET(test_path))
    assert.res_status(200, res)

    assert.is_nil(res.headers["X-Ratelimit-Limit-Minute"])
    assert.is_nil(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.is_nil(res.headers["Ratelimit-Limit"])
    assert.is_nil(res.headers["Ratelimit-Remaining"])
    assert.is_nil(res.headers["Ratelimit-Reset"])
    assert.is_nil(res.headers["Retry-After"])

    -- repeat until get rate-limited
    helpers.wait_until(function()
      res = assert(GET(test_path))
      return res.status == 429, "should be rate-limited (429), got " .. res.status
    end, 10)

    assert.res_status(429, res)
    assert.is_nil(res.headers["X-Ratelimit-Limit-Minute"])
    assert.is_nil(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.is_nil(res.headers["Ratelimit-Limit"])
    assert.is_nil(res.headers["Ratelimit-Remaining"])
    assert.is_nil(res.headers["Ratelimit-Reset"])
    assert.is_nil(res.headers["Retry-After"])
  end)

  it("handles multiple limits (single ip)", function()
    local test_path = "/test"
    local test_header = "test-header"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      hour                = 99999,
      policy              = policy,
      limit_by            = limit_by,
      path                = test_path,
      header_name         = test_header,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local function proxy_fn()
      return GET(test_path)
    end

    retry(function ()
      validate_headers(client_requests(7, proxy_fn), true, true)
    end)

  end)

  it("expire counter", function()
    local test_path = "/test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      second              = 1,
      policy              = policy,
      limit_by            = "ip",
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    assert
      .with_timeout(15)
      .with_max_tries(10)
      .with_step(0.5) -- the windows is 1 second, we wait 0.5 seconds between each retry,
                      -- that can avoid some unlucky case (we are at the end of the window)
      .ignore_exceptions(false)
      .eventually(function()
        local res1 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
        assert.res_status(200, res1)
        assert.are.same(1, tonumber(res1.headers["RateLimit-Limit"]))
        assert.are.same(0, tonumber(res1.headers["RateLimit-Remaining"]))
        assert.is_true(tonumber(res1.headers["ratelimit-reset"]) >= 0)
        assert.are.same(1, tonumber(res1.headers["X-RateLimit-Limit-Second"]))
        assert.are.same(0, tonumber(res1.headers["X-RateLimit-Remaining-Second"]))

        local res2 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
        local body2 = assert.res_status(429, res2)
        local json2 = cjson.decode(body2)
        assert.not_nil(json2.message)
        assert.matches("API rate limit exceeded", json2.message)

        ngx_sleep(1)
        local res3 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
        assert.res_status(200, res3)
      end)
      .has_no_error("counter should have been cleared after current window")
  end)

  it("blocks with a custom error code and message", function()
    local test_path = "/test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 1,
      policy              = policy,
      limit_by            = limit_by,
      path                = test_path,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      },
      error_code          = 404,
      error_message       = "Fake Not Found",
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local res = GET(test_path)
    assert.res_status(200, res)

    helpers.wait_timer("rate-limiting", true, "any-finish")

    res = GET(test_path)
    local json = cjson.decode(assert.res_status(404, res))
    assert.matches("Fake Not Found", json.message)

  end)      -- it("blocks with a custom error code and message", function()
end         -- if limit_by == "ip" then

if limit_by == "service" then
  it("blocks if exceeding limit (multiple service)", function ()
    local test_path_1, test_path_2 = "/1-test", "/2-test"

    local service_1, service_2 = setup_service(admin_client, UPSTREAM_URL),
                                 setup_service(admin_client, UPSTREAM_URL)
    local route_1, route_2 = setup_route(admin_client, service_1, { test_path_1 }),
                             setup_route(admin_client, service_2, { test_path_2 })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "service",
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    })

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route_1)
      delete_route(admin_client, route_2)
      delete_service(admin_client, service_1)
      delete_service(admin_client, service_2)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      for _, path in ipairs({ test_path_1, test_path_2 }) do
        validate_headers(client_requests(7, function()
          return GET(path)
        end), true)
      end     -- for _, path in ipairs({ test_path_1, test_path_2 }) do
    end)      -- retry(function ()
  end)        -- it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function ()
end           -- if limit_by == "service" then

if limit_by == "path" then
  it("blocks if exceeding limit (multiple path)", function()
    local test_path_1, test_path_2 = "/1-test", "/2-test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route_1, route_2 = setup_route(admin_client, service, { test_path_1 }),
                             setup_route(admin_client, service, { test_path_2 })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "path",
      path                = test_path_1,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route_1)
      delete_route(admin_client, route_2)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      for _, path in ipairs({ test_path_1, test_path_2 }) do
        validate_headers(client_requests(7, function()
          return GET(path)
        end), true)
      end     -- for _, path in ipairs({ test_path_1, test_path_2 }) do
    end)      -- retry(function ()

  end)        -- it("blocks if exceeding limit (multiple path)", function()
end           -- if limit_by == "path" then

if limit_by == "header" then
  it("blocks if exceeding limit (multiple header)", function()
    local test_path = "/test"
    local test_header_1, test_header_2 = "test-header-1", "test-header-2"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = "header",
      header_name         = test_header_1,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      for _, header_name in ipairs({ test_header_1, test_header_2 }) do
        validate_headers(client_requests(7, function()
          return GET(test_path, { headers = { [header_name] = "test" }})
        end), true)
      end     -- for _, header_name in ipairs({ test_header_1, test_header_2 }) do
    end)      -- retry(function ()

  end)        -- it("blocks if exceeding limit (multiple header)", function()
end           -- if limit_by == "header" then

if limit_by == "consumer" or limit_by == "credential" then
  it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function()
    local test_path = "/test"
    local test_key_name = "test-key"
    local test_credential_1, test_credential_2 = "test_credential_1", "test_credential_2"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = policy,
      limit_by            = limit_by,
      redis = {
        host          = REDIS_HOST,
        port          = ssl_conf.redis_port,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = ssl_conf.redis_ssl,
        ssl_verify    = ssl_conf.redis_ssl_verify,
        server_name   = ssl_conf.redis_server_name,
      }
    }, service)
    local auth_plugin = setup_key_auth_plugin(admin_client, {
      key_names = { test_key_name },
    }, service)
    local consumer_1, consumer_2 = setup_consumer(admin_client, "Bob"), setup_consumer(admin_client, "Alice")
    local credential_1, credential_2 = setup_credential(admin_client, consumer_1, test_credential_1),
                                       setup_credential(admin_client, consumer_2, test_credential_2)

    finally(function()
      delete_credential(admin_client, credential_1)
      delete_credential(admin_client, credential_2)
      delete_consumer(admin_client, consumer_1)
      delete_consumer(admin_client, consumer_2)
      delete_plugin(admin_client, auth_plugin)
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function()
      for _, credential in ipairs({ test_credential_1, test_credential_2 }) do
        validate_headers(client_requests(7, function()
          return GET(test_path, { headers = { [test_key_name] = credential }})
        end), true)
      end     -- for _, credential in ipairs({ test_credential_1, test_credential_2 }) do
    end)      -- retry(function()

  end)        -- it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function()
end           -- if limit_by == "consumer" and limit_by == "credential" then

end)

::continue::

end     -- for ssl_conf_name, ssl_conf in pairs(ssl_confs) do
end     -- for ___, limit_by in ipairs(limit_by_confs) do

desc = fmt("Plugin: rate-limiting fault tolerancy #db (access) [strategy: %s] [policy: %s]",
                 strategy, policy)

describe(desc, function ()
  local db, https_server, admin_client
  local test_path = "/test"
  local service
  local UPSTREAM_PORT
  local UPSTREAM_URL

  local function start_kong()
    return helpers.start_kong({
      database   = strategy,
      nginx_conf = "spec/fixtures/custom_nginx.template",
      plugins = "bundled,rate-limiting,key-auth",
      trusted_ips = "0.0.0.0/0,::/0",
      lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
    })
  end

  local stop_kong = helpers.stop_kong

  lazy_setup(function()
    UPSTREAM_PORT = helpers.get_available_port()
    UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)

    https_server = helpers.https_server.new(UPSTREAM_PORT)
    https_server:start()
  end)


  lazy_teardown(function()
    assert(https_server, "unexpected error")
    https_server:shutdown()
    local _
    _, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
    db:reset()
  end)

  before_each(function ()
    local _
    _, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
    db:reset()
    _, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })

    if policy == "redis" then
      redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)

    elseif policy == "cluster" then
      db:truncate("ratelimiting_metrics")
    end

    assert(start_kong())

    admin_client = helpers.admin_client()

    service = setup_service(admin_client, UPSTREAM_URL)
    setup_route(admin_client, service, { test_path })

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })
  end)

  after_each(function()
    admin_client:close()
    assert(stop_kong())
  end)


if policy == "cluster" then
  it("does not work if an error occurs", function ()
    setup_rl_plugin(admin_client, {
      minute              = 6,
      limit_by            = "ip",
      policy              = policy,
      fault_tolerant      = false,
    }, service)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function()
      local ret = client_requests(2, function()
        return GET(test_path)
      end)

      assert.same({6, 6}, ret.minute_limit)
      assert.same({5, 4}, ret.minute_remaining)
      assert.same({6, 6}, ret.limit)
      assert.same({5, 4}, ret.remaining)

      for _, reset in ipairs(ret.reset) do
        assert.equal(true, reset <= 60 and reset >= 0)
      end
    end)

    assert(db.connector:query("DROP TABLE ratelimiting_metrics"))

    local res = assert(GET(test_path))
    local body = assert.res_status(500, res)
    local json = cjson.decode(body)
    assert.not_nil(json)
    assert.matches("An unexpected error occurred", json.message)

    assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
    assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.falsy(res.headers["Ratelimit-Limit"])
    assert.falsy(res.headers["Ratelimit-Remaining"])
    assert.falsy(res.headers["Ratelimit-Reset"])
  end)

  it("keeps working if an error occurs", function ()
    setup_rl_plugin(admin_client, {
      minute              = 6,
      limit_by            = "ip",
      policy              = policy,
      fault_tolerant      = true,
    }, service)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function()
      local ret = client_requests(2, function()
        return GET(test_path)
      end)

      assert.same({6, 6}, ret.minute_limit)
      assert.same({5, 4}, ret.minute_remaining)
      assert.same({6, 6}, ret.limit)
      assert.same({5, 4}, ret.remaining)
      assert.same({200, 200}, ret.status)

      for _, reset in ipairs(ret.reset) do
        assert.equal(true, reset <= 60 and reset >= 0)
      end
    end)

    assert(db.connector:query("DROP TABLE ratelimiting_metrics"))

    local res = assert(GET(test_path))
    assert.res_status(200, res)
    assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
    assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.falsy(res.headers["Ratelimit-Limit"])
    assert.falsy(res.headers["Ratelimit-Remaining"])
    assert.falsy(res.headers["Ratelimit-Reset"])

  end)
end     -- if policy == "cluster" then

if policy == "redis" then
  it("does not work if an error occurs", function ()
    setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = "redis",
      limit_by            = "ip",
      redis = {
        host          = "127.0.0.1",
        port          = 80,                     -- bad redis port
        ssl           = false,
      },
      fault_tolerant      = false,
    }, service)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local res = assert(GET(test_path))
    local body = assert.res_status(500, res)
    local json = cjson.decode(body)
    assert.not_nil(json)
    assert.matches("An unexpected error occurred", json.message)

    assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
    assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.falsy(res.headers["Ratelimit-Limit"])
    assert.falsy(res.headers["Ratelimit-Remaining"])
    assert.falsy(res.headers["Ratelimit-Reset"])
  end)

  it("keeps working if an error occurs", function ()
    setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = "redis",
      limit_by            = "ip",
      redis = {
        host          = "127.0.0.1",
        port          = 80,                     -- bad redis port
        ssl           = false,
      },
      fault_tolerant      = true,
    }, service)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    local res = assert(GET(test_path))
    assert.res_status(200, res)

    assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
    assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
    assert.falsy(res.headers["Ratelimit-Limit"])
    assert.falsy(res.headers["Ratelimit-Remaining"])
    assert.falsy(res.headers["Ratelimit-Reset"])

  end)
end     -- if policy == "redis" then

end)

if policy == "redis" then

desc = fmt("Plugin: rate-limiting with sync_rate #db (access) [strategy: %s]", strategy)

describe(desc, function ()
  local https_server, admin_client

  local UPSTREAM_PORT
  local UPSTREAM_URL

  lazy_setup(function()
    UPSTREAM_PORT = helpers.get_available_port()
    UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)

    helpers.get_db_utils(strategy, nil, {
      "rate-limiting",
    })

    https_server = helpers.https_server.new(UPSTREAM_PORT)
    https_server:start()

    assert(helpers.start_kong({
      database   = strategy,
      nginx_conf = "spec/fixtures/custom_nginx.template",
      plugins = "bundled,rate-limiting,key-auth",
      trusted_ips = "0.0.0.0/0,::/0",
      lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
      log_level = "error"
    }))

  end)

  lazy_teardown(function()
    https_server:shutdown()
    assert(helpers.stop_kong())
  end)

  before_each(function()
    redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
    admin_client = helpers.admin_client()
  end)

  after_each(function()
    admin_client:close()
  end)

  it("blocks if exceeding limit", function ()
    local test_path = "/test"

    local service = setup_service(admin_client, UPSTREAM_URL)
    local route = setup_route(admin_client, service, { test_path })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = "redis",
      limit_by            = "ip",
      redis = {
        host          = REDIS_HOST,
        port          = REDIS_PORT,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = false,
      },
      sync_rate           = 10,
    }, service)
    local red = redis_helper.connect(REDIS_HOST, REDIS_PORT)
    local ok, err = red:select(REDIS_DATABASE)
    if not ok then
      error("failed to change Redis database: " .. err)
    end

    finally(function()
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route)
      delete_service(admin_client, service)
      red:close()
      local shell = require "resty.shell"
      shell.run("cat servroot/logs/error.log", nil, 0)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
    })

    -- initially, the metrics are not written to the redis
    assert(red:dbsize() == 0, "redis db should be empty, but got " .. red:dbsize())

    retry(function ()
      -- exceed the limit
      for _i = 0, 7 do
        GET(test_path)
      end

      -- exceed the limit locally
      assert.res_status(429, GET(test_path))

      -- wait for the metrics to be written to the redis
      helpers.pwait_until(function()
        GET(test_path)
        assert(red:dbsize() == 1, "redis db should have 1 key, but got " .. red:dbsize())
      end, 15)

      -- wait for the metrics expire
      helpers.pwait_until(function()
        assert.res_status(200, GET(test_path))
      end, 61)

    end)

  end)  -- it("blocks if exceeding limit", function ()

end)

end     -- if policy == "redis" then

end     -- for __, policy in ipairs({ "local", "cluster", "redis" }) do


desc = fmt("Plugin: rate-limiting enable globally #db (access) [strategy: %s]", strategy)

describe(desc, function ()
  local https_server, admin_client

  local UPSTREAM_PORT
  local UPSTREAM_URL

  lazy_setup(function()
    UPSTREAM_PORT = helpers.get_available_port()
    UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)

    helpers.get_db_utils(strategy, nil, {
      "rate-limiting", "key-auth",
    })

    https_server = helpers.https_server.new(UPSTREAM_PORT)
    https_server:start()

    assert(helpers.start_kong({
      database   = strategy,
      nginx_conf = "spec/fixtures/custom_nginx.template",
      plugins = "bundled,rate-limiting,key-auth",
      trusted_ips = "0.0.0.0/0,::/0",
      lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
    }))

  end)

  lazy_teardown(function()
    https_server:shutdown()
    assert(helpers.stop_kong())
  end)

  before_each(function()
    admin_client = helpers.admin_client()
  end)

  after_each(function()
    admin_client:close()
  end)

  it("global for single consumer", function()
    local test_path_1, test_path_2 = "/1-test", "/2-test"
    local test_key_name = "test-key"
    local test_credential = "test-credential"

    local service_1, service_2 = setup_service(admin_client, UPSTREAM_URL),
                                 setup_service(admin_client, UPSTREAM_URL)
    local route_1, route_2 = setup_route(admin_client, service_1, { test_path_1 }),
                             setup_route(admin_client, service_2, { test_path_2 })
    local consumer = setup_consumer(admin_client, "Bob")
    local key_auth_plugin = setup_key_auth_plugin(admin_client, {
      key_names = { test_key_name },
    })
    local rl_plugin = setup_rl_plugin(admin_client, {
      minute              = 6,
      policy              = "local",
      limit_by            = "credential",
      redis = {
        host          = REDIS_HOST,
        port          = REDIS_PORT,
        password      = REDIS_PASSWORD,
        database      = REDIS_DATABASE,
        ssl           = false,
      }
    })
    local credential = setup_credential(admin_client, consumer, test_credential)

    finally(function()
      delete_credential(admin_client, credential)
      delete_consumer(admin_client, consumer)
      delete_plugin(admin_client, key_auth_plugin)
      delete_plugin(admin_client, rl_plugin)
      delete_route(admin_client, route_1)
      delete_route(admin_client, route_2)
      delete_service(admin_client, service_1)
      delete_service(admin_client, service_2)
    end)

    helpers.wait_for_all_config_update({
      override_global_rate_limiting_plugin = true,
      override_global_key_auth_plugin = true,
    })

    retry(function ()
      validate_headers(client_requests(7, function()
        return GET(test_path_1, {
          headers = {
            [test_key_name] = test_credential,
          }
        })
      end), true)

      local ret = client_requests(7, function()
        return GET(test_path_2, {
          headers = {
            [test_key_name] = test_credential,
          }
        })
      end)

      assert.same({
        6, 6, 6, 6, 6, 6, 6,
      }, ret.minute_limit)

      assert.same({
        0, 0, 0, 0, 0, 0, 0,
      }, ret.minute_remaining)

      assert.same({
        6, 6, 6, 6, 6, 6, 6,
      }, ret.limit)

      assert.same({
        0, 0, 0, 0, 0, 0, 0,
      }, ret.remaining)

      assert.same({
        429, 429, 429, 429, 429, 429, 429,
      }, ret.status)

      for _, reset in ipairs(ret.reset) do
        assert.equal(true, reset <= 60 and reset >= 0)
      end
    end)

  end)
end)

end     -- for _, strategy in helpers.each_strategy() do
