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

local slots_default, slots_max = 10000, 2^16

local function it_content_types(title, fn)
  local test_form_encoded = fn("application/x-www-form-urlencoded")
  local test_multipart = fn("multipart/form-data")
  local test_json = fn("application/json")

  it(title .. " with application/www-form-urlencoded", test_form_encoded)
  it(title .. " with multipart/form-data", test_multipart)
  it(title .. " with application/json", test_json)
end

for _, strategy in helpers.each_strategy() do

describe("Admin API: #" .. strategy, function()
  local client
  local bp
  local db

  lazy_setup(function()

    bp, db = helpers.get_db_utils(strategy, {})

    assert(helpers.start_kong{
      database = strategy
    })
    client = assert(helpers.admin_client())
  end)

  lazy_teardown(function()
    if client then client:close() end
    helpers.stop_kong()
  end)

  describe("/upstreams #" .. strategy, function()
    describe("POST", function()
      before_each(function()
        assert(db:truncate("upstreams"))
      end)
      it_content_types("creates an upstream with defaults", function(content_type)
        return function()
          local res = client:post("/upstreams", {
            body = { name = "my.upstream" },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal(slots_default, json.slots)
          assert.are.equal("none", json.hash_on)
          assert.are.equal("none", json.hash_fallback)
          assert.equal(ngx.null, json.hash_on_header)
          assert.equal(ngx.null, json.hash_fallback_header)
        end
      end)
      it_content_types("creates an upstream without defaults", function(content_type)
        return function()
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my.upstream",
              slots = 10,
              hash_on = "consumer",
              hash_fallback = "ip",
              hash_on_header = "HeaderName",
              hash_fallback_header = "HeaderFallback",
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal(10, json.slots)
          assert.are.equal("consumer", json.hash_on)
          assert.are.equal("ip", json.hash_fallback)
          assert.are.equal("HeaderName", json.hash_on_header)
          assert.are.equal("HeaderFallback", json.hash_fallback_header)
        end
      end)
      it_content_types("creates an upstream with hash_on cookie parameters", function(content_type)
        return function()
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my.upstream",
              hash_on = "cookie",
              hash_on_cookie = "CookieName1",
              hash_on_cookie_path = "/foo",
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal("cookie", json.hash_on)
          assert.are.equal("CookieName1", json.hash_on_cookie)
          assert.are.equal("/foo", json.hash_on_cookie_path)
        end
      end)
      it_content_types("creates an upstream with 2 header hashes", function(content_type)
        return function()
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my.upstream",
              slots = 10,
              hash_on = "header",
              hash_fallback = "header",
              hash_on_header = "HeaderName1",
              hash_fallback_header = "HeaderName2",
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal(10, json.slots)
          assert.are.equal("header", json.hash_on)
          assert.are.equal("header", json.hash_fallback)
          assert.are.equal("HeaderName1", json.hash_on_header)
          assert.are.equal("HeaderName2", json.hash_fallback_header)
        end
      end)
      it_content_types("creates an upstream with " .. slots_max .. " slots", function(content_type)
        return function()
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my.upstream",
              slots = slots_max,
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal(slots_max, json.slots)
        end
      end)
      it_content_types("creates an upstream with host header", function(content_type)
        return function()
          local res = client:post("/upstreams", {
            body = { name = "my.upstream", host_header = "localhost" },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my.upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.are.equal(slots_default, json.slots)
          assert.are.equal("none", json.hash_on)
          assert.are.equal("none", json.hash_fallback)
          assert.equal(ngx.null, json.hash_on_header)
          assert.equal(ngx.null, json.hash_fallback_header)
          assert.equal("localhost", json.host_header)
        end
      end)
      describe("errors", function()
        it("handles malformed JSON body", function()
          local res = assert(client:request {
            method = "POST",
            path = "/upstreams",
            body = '{"hello": "world"',
            headers = {["Content-Type"] = "application/json"}
          })
          local body = assert.res_status(400, res)
          assert.equal('{"message":"Cannot parse JSON body"}', body)
        end)
        it_content_types("handles invalid input", function(content_type)
          return function()
            -- Missing parameter
            local res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                slots = 50,
              },
              headers = {["Content-Type"] = content_type}
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("schema violation", json.name)
            assert.same({ name = "required field missing" }, json.fields)

            -- Invalid name parameter
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "some invalid host name",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("schema violation", json.name)
            assert.same({ name = "Invalid name ('some invalid host name'); must be a valid hostname" }, json.fields)

            -- Invalid slots parameter
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                slots = 2^16+1
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("schema violation", json.name)
            assert.same({ slots = "value should be between 10 and 65536" }, json.fields)

            -- Invalid hash_on entries
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "something that is invalid",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("schema violation", json.name)
            assert.same({ hash_on = "expected one of: none, consumer, ip, header, cookie, path, query_arg, uri_capture" }, json.fields)

            -- Invalid hash_fallback entries
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "something that is invalid",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("schema violation", json.name)
            assert.same({
              ["@entity"] = { [[failed conditional validation given value of field 'hash_on']] },
              hash_fallback = "expected one of: none, ip, header, cookie, path, query_arg, uri_capture",
            }, json.fields)

            -- same hash entries
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "consumer",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = { [[failed conditional validation given value of field 'hash_on']] },
              hash_fallback = "expected one of: none, ip, header, cookie, path, query_arg, uri_capture",
            }, json.fields)

            -- Invalid header
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "header",
                hash_fallback = "consumer",
                hash_on_header = "not a <> valid <> header name",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("bad header name 'not a <> valid <> header name', allowed characters are A-Z, a-z, 0-9, '_', and '-'",
                          json.fields.hash_on_header)

            -- Invalid fallback header
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "header",
                hash_fallback_header = "not a <> valid <> header name",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals("bad header name 'not a <> valid <> header name', allowed characters are A-Z, a-z, 0-9, '_', and '-'",
                          json.fields.hash_fallback_header)

            -- Same headers
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "header",
                hash_fallback = "header",
                hash_on_header = "headername",
                hash_fallback_header = "headername",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equal("schema violation", json.name)
            assert.same({ ["@entity"] = { "values of these fields must be distinct: 'hash_on_header', 'hash_fallback_header'" }, }, json.fields)

            -- Cookie with hash_fallback
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "cookie",
                hash_on_cookie = "cookiename",
                hash_fallback = "header",
                hash_fallback_header = "Cool-Header",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = { [[failed conditional validation given value of field 'hash_on']] },
              hash_fallback = "expected one of: none",
            }, json.fields)

            -- No headername provided
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "header",
                hash_fallback = "header",
                hash_on_header = nil,  -- not given
                hash_fallback_header = "HeaderName",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = { [[failed conditional validation given value of field 'hash_on']] },
              hash_on_header = "required field missing",
            }, json.fields)

            -- No fallback headername provided
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "header",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = { [[failed conditional validation given value of field 'hash_fallback']] },
              hash_fallback_header = "required field missing",
            }, json.fields)

            -- Invalid cookie
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "cookie",
                hash_on_cookie = "not a <> valid <> cookie name",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals([[contains one or more invalid characters. ASCII control characters (0-31;127), space, tab and the characters ()<>@,;:\"/?={}[] are not allowed.]],
                          json.fields.hash_on_cookie)

            -- Invalid cookie path
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "cookie",
                hash_on_cookie = "hashme",
                hash_on_cookie_path = "not a path",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ hash_on_cookie_path = "should start with: /" }, json.fields)

            -- Invalid cookie in hash fallback
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "cookie",
                hash_on_cookie = "not a <> valid <> cookie name",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.equals([[contains one or more invalid characters. ASCII control characters (0-31;127), space, tab and the characters ()<>@,;:\"/?={}[] are not allowed.]],
                          json.fields.hash_on_cookie)

            -- Invalid cookie path in hash fallback
            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
                hash_on = "consumer",
                hash_fallback = "cookie",
                hash_on_cookie = "my-cookie",
                hash_on_cookie_path = "not a path",
              },
              headers = {["Content-Type"] = content_type}
            })
            body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ hash_on_cookie_path = "should start with: /" }, json.fields)

          end
        end)
        it_content_types("returns 409 on conflict", function(content_type)
          return function()
            local res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
              },
              headers = {["Content-Type"] = content_type}
            })
            assert.res_status(201, res)

            res = assert(client:send {
              method = "POST",
              path = "/upstreams",
              body = {
                name = "my.upstream",
              },
              headers = {["Content-Type"] = content_type}
            })
            local body = assert.res_status(409, res)
            local json = cjson.decode(body)
            assert.equal("unique constraint violation", json.name)
            assert.same({ name = "my.upstream" }, json.fields)
          end
        end)
      end)
    end)

    describe("PUT", function()
      before_each(function()
        assert(db:truncate("upstreams"))
      end)

      it_content_types("creates if not exists", function(content_type)
        return function()
          local res = assert(client:send {
            method = "PUT",
            path = "/upstreams/my-upstream",
            body = {
              created_at = 1461276890000
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(200)
          local json = assert.response(res).has.jsonbody()
          assert.equal("my-upstream", json.name)
          assert.is_number(json.created_at)
          assert.is_string(json.id)
          assert.is_number(json.slots)
        end
      end)
      it_content_types("replaces if exists", function(content_type)
        return function()
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my-upstream",
              slots = 100,
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()

          res = assert(client:send {
            method = "PUT",
            path = "/upstreams/" .. json.id,
            body = {
              name = "my-new-upstream",
              slots = 123,
              created_at = json.created_at
            },
            headers = {["Content-Type"] = content_type}
          })
          assert.response(res).has.status(200)
          local updated_json = assert.response(res).has.jsonbody()
          assert.equal("my-new-upstream", updated_json.name)
          assert.equal(123, updated_json.slots)
          assert.equal(json.id, updated_json.id)
        end
      end)
      describe("errors", function()
        it_content_types("handles invalid input", function(content_type)
          return function()
            -- Missing parameter
            local res = assert(client:send {
              method = "PUT",
              path = "/upstreams/00000000-0000-0000-0000-000000000001",
              body = {},
              headers = {["Content-Type"] = content_type}
            })
            local body = assert.response(res).has.status(400)
            local json = cjson.decode(body)
            assert.same("schema violation (name: required field missing)", json.message)

            -- Invalid parameter
            local res = assert(client:send {
              method = "PUT",
              path = "/upstreams/1.2.3.4", -- ip is not allowed
              body = { created_at = 1461276890000 },
              headers = {["Content-Type"] = content_type}
            })

            body = assert.response(res).has.status(400)
            local json = cjson.decode(body)
            assert.same("Invalid name ('1.2.3.4'); no ip addresses allowed", json.message)
          end
        end)
      end)
    end)

    describe("GET", function()
      lazy_setup(function()
        assert(db:truncate("upstreams"))
        bp.upstreams:insert_n(10)
      end)
      lazy_teardown(function()
        assert(db:truncate("upstreams"))
      end)

      it("retrieves the first page", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams"
        })
        assert.response(res).has.status(200)
        local json = assert.response(res).has.jsonbody()
        assert.equal(10, #json.data)
      end)
      it("paginates a set", function()
        local pages = {}
        local offset

        for i = 1, 4 do
          local res = assert(client:send {
            method = "GET",
            path = "/upstreams",
            query = {size = 3, offset = offset}
          })
          assert.response(res).has.status(200)
          local json = assert.response(res).has.jsonbody()

          if i < 4 then
            assert.equal(3, #json.data)
          else
            assert.equal(1, #json.data)
          end

          if i > 1 then
            -- check all pages are different
            assert.not_same(pages[i-1], json)
          end

          offset = json.offset
          pages[i] = json
        end
      end)
      it("ignores filters", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams",
          query = {foo = "bar"}
        })
        assert.res_status(200, res)
      end)
      it("ignores an invalid body", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams",
          body = "this fails if decoded as json",
          headers = {
            ["Content-Type"] = "application/json",
          }
        })
        assert.res_status(200, res)
      end)
      it("returns bad request for empty tags", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams",
          query = { tags = ngx.null}
        })
        res = assert.res_status(400, res)
        local json = cjson.decode(res)
        assert.same("invalid option (tags: cannot be null)", json.message)
      end)

      describe("empty results", function()
        lazy_setup(function()
          assert(db:truncate("upstreams"))
        end)

        it("data property is an empty array", function()
          local res = assert(client:send {
            method = "GET",
            path = "/upstreams"
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.same({ data = {}, next = ngx.null }, json)
        end)
      end)
    end)

    describe("DELETE", function()
      it("by id", function(content_type)
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my-upstream",
              slots = 100,
            },
            headers = { ["Content-Type"] = "application/json" }
          })

          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()

          res = assert(client:send {
            method = "DELETE",
            path = "/upstreams/" .. json.id,
          })

          assert.response(res).has.status(204)
      end)

      it("by name", function(content_type)
          local res = assert(client:send {
            method = "POST",
            path = "/upstreams",
            body = {
              name = "my-upstream",
              slots = 100,
            },
            headers = { ["Content-Type"] = "application/json" }
          })

          assert.response(res).has.status(201)
          local json = assert.response(res).has.jsonbody()

          res = assert(client:send {
            method = "DELETE",
            path = "/upstreams/" .. json.name,
          })

          assert.response(res).has.status(204)
      end)

      it("after deleting its targets (regression test for #4317)", function(content_type)
        assert(db:truncate("upstreams"))
        assert(db:truncate("targets"))

        client = assert(helpers.admin_client())
        -- create the upstream
        local res = assert(client:send {
          method = "POST",
          path = "/upstreams",
          body = {
            name = "my-upstream",
            slots = 100,
          },
          headers = { ["Content-Type"] = "application/json" }
        })

        assert.response(res).has.status(201)

        client:close()
        client = assert(helpers.admin_client())

        -- create the target
        local res = assert(client:post("/upstreams/my-upstream/targets", {
          body = {
            target = "127.0.0.1:8000",
          },
          headers = { ["Content-Type"] = "application/json" }
        }))

        assert.response(res).has.status(201)

        client:close()
        client = assert(helpers.admin_client())

        -- delete the target
        local res = assert(client:send {
          method = "DELETE",
          path = "/upstreams/my-upstream/targets/127.0.0.1:8000",
          headers = { ["Content-Type"] = "application/json" }
        })

        assert.response(res).has.status(204)

        client:close()
        client = assert(helpers.admin_client())

        -- deleting the target does not delete the upstream
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams/my-upstream",
          headers = { ["Content-Type"] = "application/json" }
        })

        assert.response(res).has.status(200)

        client:close()
        client = assert(helpers.admin_client())

        -- delete the upstream
        res = assert(client:send {
          method = "DELETE",
          path = "/upstreams/my-upstream",
        })

        assert.response(res).has.status(204)
      end)

      it("can delete an upstream with targets", function(content_type)
        assert(db:truncate("upstreams"))
        assert(db:truncate("targets"))

        -- create the upstream
        local res = assert(client:send {
          method = "POST",
          path = "/upstreams",
          body = {
            name = "my-upstream",
            slots = 100,
          },
          headers = { ["Content-Type"] = "application/json" }
        })

        assert.response(res).has.status(201)

        client:close()
        client = assert(helpers.admin_client())

        -- create the target
        local res = assert(client:post("/upstreams/my-upstream/targets", {
          body = {
            target = "127.0.0.1:8000",
          },
          headers = { ["Content-Type"] = "application/json" }
        }))

        assert.response(res).has.status(201)

        client:close()
        client = assert(helpers.admin_client())

        -- delete the upstream
        res = assert(client:send {
          method = "DELETE",
          path = "/upstreams/my-upstream",
        })

        assert.response(res).has.status(204)
      end)
    end)

    it("returns 405 on invalid method", function()
      local res = assert(client:send {
        method = "DELETE",
        path = "/upstreams",
        headers = { ["Content-Type"] = "application/json" }
      })

      local body = assert.response(res).has.status(405)
      local json = cjson.decode(body)
      assert.same({ message = "Method not allowed" }, json)
    end)
  end)
end)

end

for _, strategy in helpers.all_strategies() do
  describe("#regression #" .. strategy, function()
    local client
    lazy_setup(function()
      local bp, _ = helpers.get_db_utils(strategy)
      bp.upstreams:insert {
        name = "my-upstream",
        slots = 100,
      }
      bp.upstreams:insert {
        name = "my-upstream-2",
        slots = 100,
      }
      bp.upstreams:insert {
        name = "my-upstream-3",
        slots = 100,
      }

      assert(helpers.start_kong{
        database = strategy
      })
      client = assert(helpers.admin_client())
    end)

    lazy_teardown(function()
      if client then client:close() end
      helpers.stop_kong()
    end)

    it("page size 1", function()
      local res = assert(client:send {
        method = "GET",
        path = "/upstreams?size=1"
      })
      assert.response(res).has.status(200)
      local json = assert.response(res).has.jsonbody()
      assert.equal(1, #json.data)
      assert.truthy(json.offset)

      res = assert(client:send {
        method = "GET",
        path = "/upstreams",
        query = {size = 1, offset = json.offset}
      })
      assert.response(res).has.status(200)
      local json = assert.response(res).has.jsonbody()
      assert.equal(1, #json.data)
      assert.truthy(json.offset)

      res = assert(client:send {
        method = "GET",
        path = "/upstreams?size=2"
      })
      assert.response(res).has.status(200)
      local json = assert.response(res).has.jsonbody()
      assert.equal(2, #json.data)
      assert.truthy(json.offset)
    end)
  end)
end