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

local function client_send(req)
  local client = helpers.http_client("127.0.0.1", 9500, 20000)
  local res = assert(client:send(req))
  local status, body = res.status, res:read_body()
  client:close()
  return status, body
end

local strategy = "off"

describe("Status API #" .. strategy, function()
  local bp
  local client

  local apis, healthcheck_apis = {}, {}

  local upstream, empty_upstream, healthcheck_upstream

  lazy_setup(function()
    local fixtures = {
      dns_mock = helpers.dns_mock.new()
    }
    fixtures.dns_mock:A {
      name = "custom_localhost",
      address = "127.0.0.1",
    }

    bp = helpers.get_db_utils(strategy, {
      "upstreams",
      "targets",
    })

    upstream = bp.upstreams:insert {}

    for i=1, 4 do
      apis[i] = bp.targets:insert {
        target = string.format("api-%d:80", i),
        weight = 10 * i,
        upstream = { id = upstream.id },
      }
    end

    healthcheck_upstream = bp.upstreams:insert {
      healthchecks = {
        passive = {
          healthy = {
            successes = 1,
          },
          unhealthy = {
            tcp_failures = 1,
            http_failures = 1,
            timeouts = 1,
          },
        }
      }
    }

    for i=1, 4 do
      healthcheck_apis[i] = bp.targets:insert {
        target = string.format("hc-api-%d:80", i),
        weight = 10 * i,
        upstream = { id = healthcheck_upstream.id },
      }
    end

    empty_upstream = bp.upstreams:insert {}

    assert(helpers.start_kong({
      status_listen = "127.0.0.1:9500",
      database   = strategy,
      nginx_conf = "spec/fixtures/custom_nginx.template",
    }, nil, nil, fixtures))
  end)

  lazy_teardown(function()
    assert(helpers.stop_kong())
  end)

  before_each(function()
    client = assert(helpers.http_client("127.0.0.1", 9500, 20000))
  end)

  after_each(function()
    if client then client:close() end
  end)

  describe("/upstreams/{upstream}/targets/", function()

    describe("POST", function()
      it("is not exposed", function()
        local res = assert(client:post(
          "/upstreams/" .. upstream.name .. "/targets/", {
          body = {
            target = "mashape.com",
          },
          headers = {["Content-Type"] = "application/json"}
        }))
        assert.response(res).has.status(405)
      end)
    end)

    describe("GET", function()
      it("shows all targets", function()
        for _, append in ipairs({ "", "/" }) do
          local res = assert(client:send {
            method = "GET",
            path = "/upstreams/" .. upstream.name .. "/targets" .. append,
          })
          assert.response(res).has.status(200)
          local json = assert.response(res).has.jsonbody()

          -- we got three active targets for this upstream
          assert.equal(4, #json.data)

          local apis_targets = {}
          for _, t in ipairs(apis) do
            apis_targets[t.id] = t.target
          end

          local data_targets = {}
          for _, t in ipairs(json.data) do
            data_targets[t.id] = t.target
          end

          assert.same(apis_targets, data_targets)
        end
      end)

      describe("empty results", function()
        it("data property is an empty array", function()
          local res = assert(client:send {
            method = "GET",
            path = "/upstreams/" .. empty_upstream.name .. "/targets",
          })
          local body = assert.response(res).has.status(200)
          assert.match('"data":%[%]', body)
        end)
      end)
    end)
  end)

  describe("/upstreams/{upstream}/health/", function()

    describe("GET", function()

      -- Performs tests similar to /upstreams/:upstream_id/targets,
      -- and checks for the "health" field of each target.
      -- @param targets the array of target data produced by add_targets
      -- @param n the expected number of targets in the response
      -- It is different from #targets because add_targets adds
      -- zero-weight targets as well.
      -- @param health the expected "health" response for all targets
      local function check_health_endpoint(targets, upstream, n, health)
        for _, append in ipairs({ "", "/" }) do
          local status, body = client_send({
            method = "GET",
            path = "/upstreams/" .. upstream.name .. "/health" .. append,
          })
          assert.same(200, status)
          local res = assert(cjson.decode(body))

          assert.equal(n, #res.data)


          local apis_targets = {}
          for _, t in ipairs(targets) do
            apis_targets[t.id] = t.target
          end

          local data_targets = {}
          for _, t in ipairs(res.data) do
            data_targets[t.id] = t.target
          end

          assert.same(apis_targets, data_targets)

          for i = 1, n do
            if res.data[i].data ~= nil and res.data[i].data.addresses ~= nil then
              for j = 1, #res.data[i].data.addresses do
                assert.equal(health, res.data[i].data.addresses[j].health)
              end
              assert.equal(health, res.data[i].health)
            end
          end
        end
      end

      describe("with healthchecks off", function()
        it("returns HEALTHCHECKS_OFF for targets that resolve", function()
          check_health_endpoint(apis, upstream, 4, "HEALTHCHECKS_OFF")
        end)

      end)

      describe("with healthchecks on", function()

        it("returns DNS_ERROR if DNS cannot be resolved", function()

          check_health_endpoint(healthcheck_apis, healthcheck_upstream, 4, "DNS_ERROR")

        end)

        pending("returns HEALTHY if failure not detected", function()

          check_health_endpoint(healthcheck_apis, healthcheck_upstream, 4, "HEALTHY")

        end)

      end)
    end)
  end)

  describe("/upstreams/{upstream}/targets/all/", function()

    describe("GET", function()
      it("retrieves the first page", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams/" .. upstream.name .. "/targets/all",
        })
        assert.response(res).has.status(200)
        local json = assert.response(res).has.jsonbody()
        assert.equal(4, #json.data)
      end)
      it("offset is a string", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams/" .. upstream.name .. "/targets",
          query = {size = 2},
        })
        assert.response(res).has.status(200)
        local json = assert.response(res).has.jsonbody()
        assert.is_string(json.offset)
      end)
      it("paginates a set", function()
        local pages = {}
        local offset

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

          if i < 3 then
            assert.equal(2, #json.data)
          else
            assert.equal(0, #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/" .. upstream.name .. "/targets/all",
          query = {foo = "bar"},
        })
        assert.response(res).has.status(200)
      end)
      it("ignores an invalid body", function()
        local res = assert(client:send {
          method = "GET",
          path = "/upstreams/" .. upstream.name .. "/targets/all",
          body = "this fails if decoded as json",
          headers = {
            ["Content-Type"] = "application/json",
          }
        })
        assert.response(res).has.status(200)
      end)

      describe("empty results", function()
        it("data property is an empty array", function()
          local res = assert(client:send {
            method = "GET",
            path = "/upstreams/" .. empty_upstream.name .. "/targets/all",
          })
          local body = assert.response(res).has.status(200)
          local json = cjson.decode(body)
          assert.same({
            data = {},
            next = ngx.null,
          }, json)
          -- ensure JSON representation is correct
          assert.match('"data":%[%]', body)
        end)
      end)
    end)
  end)

  describe("/upstreams/{upstream}/targets/{target}/(un)healthy", function()

    describe("POST", function()

      it("is not exposed", function()
        local status, body
        status, body = assert(client_send {
          method = "POST",
          path = "/upstreams/" .. upstream.name .. "/targets/" .. apis[1].target .. "/unhealthy"
        })
        assert.same(404, status, body)
      end)
    end)
  end)

  describe("/upstreams/{upstream}/targets/{target}", function()
    local target = apis[1]

    describe("GET", function()

      it("returns target entity", function()
        local res = client:get("/upstreams/" .. upstream.name .. "/targets/" .. target.target)
        assert.response(res).has.status(200)
        local json = assert.response(res).has.jsonbody()
        json.tags = nil
        if json.upstream.id then
          json.upstream = json.upstream.id
        end
        assert.same(target, json)
      end)
    end)

    describe("PATCH", function()

      it("is not exposed", function()
        local res = client:patch("/upstreams/" .. upstream.name .. "/targets/" .. target.target, {
          body = {
            weight = 659,
          },
          headers = { ["Content-Type"] = "application/json" }
        })
        assert.response(res).has.status(405)
      end)
    end)

    describe("PUT", function()

      it("is not exposed", function()
        local res = client:put("/upstreams/" .. upstream.name .. "/targets/" .. target.target, {
          body = {
            target = target.target
          },
          headers = { ["Content-Type"] = "application/json" }
        })

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

    end)

    describe("DELETE", function()

      it("is not exposed", function()
        local res = assert(client:send {
          method = "DELETE",
          path = "/upstreams/" .. upstream.name .. "/targets/" .. target.target
        })
        assert.response(res).has.status(405)
      end)
    end)
  end)
end)
