local hybrid_helper = require "spec.hybrid"
local cjson   = require "cjson"
local uuid    = require "kong.tools.uuid"


hybrid_helper.run_for_each_deploy({ }, function(helpers, strategy, deploy, rpc, rpc_sync)
  describe("Plugin: basic-auth (access) [" .. helpers.format_tags() .. "]", function()
    local proxy_client
    local nonexisting_anonymous = uuid.uuid() -- a non-existing consumer id

    lazy_setup(function()
      local bp = helpers.get_db_utils(strategy, {
        "routes",
        "services",
        "plugins",
        "consumers",
        "basicauth_credentials",
      })

      local consumer = bp.consumers:insert {
        username = "bob",
      }

      local anonymous_user = bp.consumers:insert {
        username = "no-body",
      }

      local route1 = bp.routes:insert {
        hosts = { "basic-auth1.test" },
      }

      local route2 = bp.routes:insert {
        hosts = { "basic-auth2.test" },
      }

      local route3 = bp.routes:insert {
        hosts = { "basic-auth3.test" },
      }

      local route4 = bp.routes:insert {
        hosts = { "basic-auth4.test" },
      }

      local route5 = bp.routes:insert {
        hosts = { "basic-auth5.test" },
      }

      local route_grpc = assert(bp.routes:insert {
        protocols = { "grpc" },
        paths = { "/hello.HelloService/" },
        service = assert(bp.services:insert {
          name = "grpc",
          url = helpers.grpcbin_url,
        }),
      })

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route1.id },
        config = {
          realm = "test-realm",
        }
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route_grpc.id },
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route2.id },
        config   = {
          hide_credentials = true,
        },
      }

      bp.basicauth_credentials:insert {
        username = "bob",
        password = "kong",
        consumer = { id = consumer.id },
      }

      bp.basicauth_credentials:insert {
        username = "user123",
        password = "password123",
        consumer = { id = consumer.id },
      }

      bp.basicauth_credentials:insert {
        username = "user321",
        password = "password:123",
        consumer = { id = consumer.id },
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route3.id },
        config   = {
          anonymous = anonymous_user.id,
        },
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route4.id },
        config   = {
          anonymous = nonexisting_anonymous, -- a non-existing consumer id
        },
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route5.id },
        config   = {
          anonymous = anonymous_user.username,
        },
      }

      assert(helpers.start_kong({
        database   = strategy,
        nginx_conf = "spec/fixtures/custom_nginx.template",
      }))

      proxy_client = helpers.proxy_client()
    end)


    lazy_teardown(function()
      if proxy_client then
        proxy_client:close()
      end

      helpers.stop_kong()
    end)

    describe("Unauthorized", function()
      describe("when realm is configured", function()
        it("returns Unauthorized on missing credentials", function()
          local res = assert(proxy_client:send {
            method  = "GET",
            path    = "/status/200",
            headers = {
              ["Host"] = "basic-auth1.test"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.not_nil(json)
          assert.matches("Unauthorized", json.message)
          assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
        end)
      end)

      describe("when realm is default", function()
        it("returns Unauthorized on missing credentials", function()
          local res = assert(proxy_client:send {
            method  = "GET",
            path    = "/status/200",
            headers = {
              ["Host"] = "basic-auth2.test"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.not_nil(json)
          assert.matches("Unauthorized", json.message)
          assert.equal('Basic realm="service"', res.headers["WWW-Authenticate"])
        end)
      end)
    end)

    describe("Unauthorized", function()

      it("returns 401 Unauthorized on invalid credentials in Authorization", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Authorization"] = "foobar",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.not_nil(json)
        assert.matches("Unauthorized", json.message)
        assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
      end)

      it("returns 401 Unauthorized on invalid credentials in Proxy-Authorization", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Proxy-Authorization"] = "foobar",
            ["Host"]                = "basic-auth1.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.not_nil(json)
        assert.matches("Unauthorized", json.message)
        assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
      end)

      it("returns 401 Unauthorized on password only", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Authorization"] = "Basic a29uZw==",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.not_nil(json)
        assert.matches("Unauthorized", json.message)
        assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
      end)

      it("returns 401 Unauthorized on username only", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Authorization"] = "Basic Ym9i",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.not_nil(json)
        assert.matches("Unauthorized", json.message)
        assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
      end)

      it("rejects gRPC call without credentials", function()
        local ok, err = helpers.proxy_client_grpc(){
          service = "hello.HelloService.SayHello",
          opts = {},
        }
        assert.falsy(ok)
        assert.matches("Code: Unauthenticated", err)
      end)

      it("accepts authorized gRPC calls", function()
        local ok, res = helpers.proxy_client_grpc(){
          service = "hello.HelloService.SayHello",
          opts = {
            ["-H"] = "'Authorization: Basic Ym9iOmtvbmc='",
          },
        }
        assert.truthy(ok)
        assert.same({ reply = "hello noname" }, cjson.decode(res))
      end)

      it("authenticates valid credentials in Authorization", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Authorization"] = "Basic Ym9iOmtvbmc=",
            ["Host"]          = "basic-auth1.test"
          }
        })
        assert.res_status(200, res)
      end)

      it("authenticates valid credentials in Authorization", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Authorization"] = "Basic dXNlcjEyMzpwYXNzd29yZDEyMw==",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal('bob', body.headers["x-consumer-username"])
        assert.equal('user123', body.headers["x-credential-identifier"])
      end)

      it("authenticates with a password containing ':'", function()
        local res = assert(proxy_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Authorization"] = "Basic dXNlcjMyMTpwYXNzd29yZDoxMjM=",
            ["Host"] = "basic-auth1.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal("bob", body.headers["x-consumer-username"])
        assert.equal("user321", body.headers["x-credential-identifier"])
      end)

      it("returns 401 for valid Base64 encoding", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Authorization"] = "Basic adXNlcjEyMzpwYXNzd29yZDEyMw==",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.not_nil(json)
        assert.matches("Unauthorized", json.message)
        assert.equal('Basic realm="test-realm"', res.headers["WWW-Authenticate"])
      end)

      it("authenticates valid credentials in Proxy-Authorization", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/status/200",
          headers = {
            ["Proxy-Authorization"] = "Basic Ym9iOmtvbmc=",
            ["Host"]                = "basic-auth1.test"
          }
        })
        assert.res_status(200, res)
      end)

    end)

    describe("Consumer headers", function()

      it("sends Consumer headers to upstream", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Authorization"] = "Basic Ym9iOmtvbmc=",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.is_string(json.headers["x-consumer-id"])
        assert.equal("bob", json.headers["x-consumer-username"])
        assert.equal("bob", json.headers["x-credential-identifier"])
      end)

    end)

    describe("config.hide_credentials", function()

      it("false sends key to upstream", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Authorization"] = "Basic Ym9iOmtvbmc=",
            ["Host"]          = "basic-auth1.test"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("Basic Ym9iOmtvbmc=", json.headers.authorization)
      end)

      it("true doesn't send key to upstream", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Authorization"] = "Basic Ym9iOmtvbmc=",
            ["Host"]          = "basic-auth2.test"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.is_nil(json.headers.authorization)
      end)

    end)


    describe("config.anonymous", function()

      it("works with right credentials and anonymous", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Authorization"] = "Basic dXNlcjEyMzpwYXNzd29yZDEyMw==",
            ["Host"]          = "basic-auth3.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal('bob', body.headers["x-consumer-username"])
        assert.equal('user123', body.headers["x-credential-identifier"])
        assert.is_nil(body.headers["x-anonymous-consumer"])
      end)

      it("works with wrong credentials and anonymous", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "basic-auth3.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal('true', body.headers["x-anonymous-consumer"])
        assert.equal('no-body', body.headers["x-consumer-username"])
        assert.equal(nil, body.headers["x-credential-identifier"])
      end)

      it("works with wrong credentials and username in anonymous", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "basic-auth5.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal('true', body.headers["x-anonymous-consumer"])
        assert.equal('no-body', body.headers["x-consumer-username"])
      end)

      it("errors when anonymous user doesn't exist", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "basic-auth4.test"
          }
        })
        local body = cjson.decode(assert.res_status(500, res))
        assert.same("anonymous consumer " .. nonexisting_anonymous .. " is configured but doesn't exist", body.message)
      end)

    end)

  end)

  describe("Plugin: basic-auth (access) [#" .. strategy .. "]", function()
    local proxy_client
    local user1
    local user2
    local anonymous

    lazy_setup(function()
      local bp = helpers.get_db_utils(strategy, {
        "routes",
        "services",
        "plugins",
        "consumers",
        "basicauth_credentials",
        "keyauth_credentials",
      })

      anonymous = bp.consumers:insert {
        username = "Anonymous",
      }

      user1 = bp.consumers:insert {
        username = "Mickey",
      }

      user2 = bp.consumers:insert {
        username = "Aladdin",
      }

      local service1 = bp.services:insert {
        path = "/request",
      }

      local service2 = bp.services:insert {
        path = "/request",
      }

      local route1 = bp.routes:insert {
        hosts   = { "logical-and.test" },
        service = service1,
      }

      local route2 = bp.routes:insert {
        hosts   = { "logical-or.test" },
        service = service2,
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route1.id },
      }

      bp.plugins:insert {
        name     = "key-auth",
        route = { id = route1.id },
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route2.id },
        config   = {
          anonymous = anonymous.id,
        },
      }

      bp.plugins:insert {
        name     = "key-auth",
        route = { id = route2.id },
        config   = {
          anonymous = anonymous.id,
        },
      }

      bp.keyauth_credentials:insert({
        key      = "Mouse",
        consumer = { id = user1.id },
      })

      bp.basicauth_credentials:insert {
        username = "Aladdin",
        password = "OpenSesame",
        consumer = { id = user2.id },
      }

      assert(helpers.start_kong({
        database   = strategy,
        nginx_conf = "spec/fixtures/custom_nginx.template",
      }))

      proxy_client = helpers.proxy_client()
    end)

    lazy_teardown(function()
      if proxy_client then
        proxy_client:close()
      end

      helpers.stop_kong()
    end)

    describe("multiple auth without anonymous, logical AND", function()

      it("passes with all credentials provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-and.test",
            ["apikey"]        = "Mouse",
            ["Authorization"] = "Basic QWxhZGRpbjpPcGVuU2VzYW1l",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert(id == user1.id or id == user2.id)
      end)

      it("fails 401, with only the first credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]   = "logical-and.test",
            ["apikey"] = "Mouse",
          }
        })
        assert.response(res).has.status(401)
      end)

      it("fails 401, with only the second credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-and.test",
            ["Authorization"] = "Basic QWxhZGRpbjpPcGVuU2VzYW1l",
          }
        })
        assert.response(res).has.status(401)
        assert.equal('Key', res.headers["WWW-Authenticate"])
      end)

      it("fails 401, with no credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "logical-and.test",
          }
        })
        assert.response(res).has.status(401)
        assert.equal('Key', res.headers["WWW-Authenticate"])
      end)

    end)

    describe("multiple auth with anonymous, logical OR", function()

      it("passes with all credentials provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-or.test",
            ["apikey"]        = "Mouse",
            ["Authorization"] = "Basic QWxhZGRpbjpPcGVuU2VzYW1l",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert(id == user1.id or id == user2.id)
      end)

      it("passes with only the first credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]   = "logical-or.test",
            ["apikey"] = "Mouse",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert.equal(user1.id, id)
      end)

      it("passes with only the second credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-or.test",
            ["Authorization"] = "Basic QWxhZGRpbjpPcGVuU2VzYW1l",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert.equal(user2.id, id)
      end)

      it("passes with no credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "logical-or.test",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.equal(id, anonymous.id)
      end)

    end)
  end)

  describe("Plugin: basic-auth (access) [#" .. strategy .. "]", function()
    local proxy_client
    local admin_client
    local anonymous

    lazy_setup(function()
      local bp = helpers.get_db_utils(strategy, {
        "routes",
        "services",
        "plugins",
        "consumers",
        "basicauth_credentials",
        "keyauth_credentials",
      })

      anonymous = bp.consumers:insert {
        username = "Anonymous",
      }

      local service = bp.services:insert {
        path = "/request",
      }

      local route = bp.routes:insert {
        hosts   = { "anonymous-with-username.test" },
        service = service,
      }

      bp.plugins:insert {
        name     = "basic-auth",
        route = { id = route.id },
        config = {
          anonymous = anonymous.username,
        },
      }

      assert(helpers.start_kong({
        database   = strategy,
        nginx_conf = "spec/fixtures/custom_nginx.template",
      }))

      proxy_client = helpers.proxy_client()
      admin_client = helpers.admin_client()
    end)

    lazy_teardown(function()
      if proxy_client then
        proxy_client:close()
      end
      if admin_client then
        admin_client:close()
      end

      helpers.stop_kong()
    end)

    it("consumer cache consistency", function()
      local res = assert(proxy_client:send {
        method = "GET",
        path = "/request",
        headers = {
          ["Host"] = "anonymous-with-username.test",
        },
      })
      assert.response(res).has.status(200)
      local body = assert.response(res).has.jsonbody()
      assert.are.equal("true", body.headers["x-anonymous-consumer"])
      assert.are.equal(anonymous.id, body.headers["x-consumer-id"])
      assert.are.equal(anonymous.username, body.headers["x-consumer-username"])

      local res = assert(admin_client:send {
        method = "DELETE",
        path = "/consumers/" .. anonymous.username,
      })
      assert.res_status(204, res)

      if strategy ~= "off" then
        helpers.wait_for_all_config_update()
      else
        ngx.sleep(1) -- wait for cache invalidation
      end

      local res = assert(proxy_client:send {
        method = "GET",
        path = "/request",
        headers = {
          ["Host"] = "anonymous-with-username.test",
        }
      })
      assert.res_status(500, res)
    end)

  end)
end)
