-- This software is copyright Kong Inc. and its licensors.
-- Use of the software is subject to the agreement between your organization
-- and Kong Inc. If there is no such agreement, use is governed by and
-- subject to the terms of the Kong Master Software License Agreement found
-- at https://konghq.com/enterprisesoftwarelicense/.
-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ]

local say = require "say"
local assert = require "luassert"

local busted = require "busted"

local conf_loader = require "kong.conf_loader"
local DB = require "kong.db"
local helpers = require "spec.helpers"
local utils = require "kong.tools.utils"

local conf = conf_loader()

local function database_type()
  return conf['database']
end

local function get_database()
  local db = assert(DB.new(conf))
  assert(db:init_connector())
  return db
end


local function database_has_relation(state, arguments)
  local table_name = arguments[1]
  local schema = arguments[2] or "public"
  local db = get_database()
  local res, err
  if database_type() == 'postgres' then
    res, err = db.connector:query(string.format(
        "select true"
        .. " from pg_tables"
        .. " where tablename = '%s'"
        .. " and schemaname = '%s'",
        table_name, schema))
  else
    return false
  end
  if err then
    return false
  end
  return not(not(res[1]))
end

say:set("assertion.database_has_relation.positive", "Expected schema to have table %s")
say:set("assertion.database_has_relation.negative", "Expected schema not to have table %s")
assert:register("assertion", "database_has_relation", database_has_relation, "assertion.database_has_relation.positive", "assertion.database_has_relation.negative")


local function database_has_trigger(state, arguments)
  local trigger_name = arguments[1]
  local db = get_database()
  local res, err
  if database_type() == 'postgres' then
    res, err = db.connector:query(string.format(
        "select true"
        .. " from pg_trigger"
        .. " where tgname = '%s'",
        trigger_name))
  else
    return false
  end
  if err then
    return false
  end
  return not(not(res[1]))
end

say:set("assertion.database_has_trigger.positive", "Expected database to have trigger %s")
say:set("assertion.database_has_trigger.negative", "Expected database not to have trigger %s")
assert:register("assertion", "database_has_trigger", database_has_trigger, "assertion.database_has_trigger.positive", "assertion.database_has_trigger.negative")


local function table_has_column(state, arguments)
  local table = arguments[1]
  local column_name = arguments[2]
  local postgres_type = arguments[3]
  local db = get_database()
  local res, err
  if database_type() == 'postgres' then
    res, err = db.connector:query(string.format(
        "select true"
        .. " from information_schema.columns"
        .. " where table_schema = 'public'"
        .. "   and table_name = '%s'"
        .. "   and column_name = '%s'"
        .. "   and data_type = '%s'",
        table, column_name, postgres_type))
  else
    return false
  end
  if err then
    return false
  end
  return not(not(res[1]))
end

say:set("assertion.table_has_column.positive", "Expected table %s to have column %s with type %s")
say:set("assertion.table_has_column.negative", "Expected table %s not to have column %s with type %s")
assert:register("assertion", "table_has_column", table_has_column, "assertion.table_has_column.positive", "assertion.table_has_column.negative")

local function table_has_index(state, arguments)
  local table = arguments[1]
  local index_name = arguments[2]
  local db = get_database()
  local res, err
  if database_type() == 'postgres' then
    res, err = db.connector:query(string.format(
        "select true"
        .. " from pg_indexes"
        .. " where schemaname = 'public'"
        .. "   and tablename = '%s'"
        .. "   and indexname = '%s'",
        table, index_name))
  else
    return false
  end
  if err then
    return false
  end
  return not(not(res[1]))
end

say:set("assertion.table_has_index.positive", "Expected table %s to have index %s")
say:set("assertion.table_has_index.negative", "Expected table %s not to index %s")
assert:register("assertion", "table_has_index", table_has_index, "assertion.table_has_index.positive", "assertion.table_has_index.negative")

local upstream_server_url = "http://" .. helpers.mock_upstream_host .. ":" .. helpers.mock_upstream_port .. "/"

local function create_example_service()
  local admin_client = assert(helpers.admin_client())
  local res = assert(admin_client:send {
      method = "POST",
      path = "/services/",
      body = {
        name = "example-service",
        url = upstream_server_url
      },
      headers = {
        ["Content-Type"] = "application/json"
      }
  })
  assert.res_status(201, res)
  res = assert(admin_client:send {
      method = "POST",
      path = "/services/example-service/routes",
      body = {
        hosts = { "example.com" },
      },
      headers = {
        ["Content-Type"] = "application/json"
      }
  })
  assert.res_status(201, res)
  admin_client:close()
end

local function send_proxy_get_request()
  local proxy_client = assert(helpers.proxy_client())
  local res = assert(proxy_client:send {
      method  = "GET",
      headers = {
        ["Host"] = "example.com",
      },
      path = "/",
  })
  local body = assert.res_status(200, res)
  proxy_client:close()

  return res, body
end

local function start_kong(options)
  return helpers.start_kong(utils.table_merge({
    database = database_type(),
    dns_resolver          = "",
    proxy_listen          = "0.0.0.0:9000",
    admin_listen          = "0.0.0.0:9001",
    admin_ssl             = false,
    admin_gui_ssl         = false,
    nginx_conf            = "spec/fixtures/custom_nginx.template",
  }, options))
end

local function it_when(phase, phrase, f)
  return busted.it(phrase .. " #" .. phase, f)
end

local function setup(f)
  return busted.it("setting up kong #setup", f)
end

local function old_after_up(phrase, f)
  return it_when("old_after_up", phrase, f)
end

local function new_after_up(phrase, f)
  return it_when("new_after_up", phrase, f)
end

local function new_after_finish(phrase, f)
  return it_when("new_after_finish", phrase, f)
end

local function all_phases(phrase, f)
  return it_when("all_phases", phrase, f)
end


--- Get a Busted test handler for migration tests.
--
-- This convenience function determines the appropriate Busted handler
-- (`busted.describe` or `busted.pending`) based on the "old Kong version"
-- that migrations are running on and the specified version range.
--
-- @function get_busted_handler
-- @param min_version The minimum Kong version (inclusive)
-- @param max_version The maximum Kong version (inclusive)
-- @return `busted.describe` if Kong's version is within the specified range,
--         `busted.pending` otherwise.
-- @usage
-- local handler = get_busted_handler("3.3.0", "3.6.0")
-- handler("some migration test", function() ... end)
local get_busted_handler
do
  local function get_version_num(v1, v2)
    if v2 then
      assert(#v2 == #v1, string.format("different version format: %s and %s", v1, v2))
    end
    return assert(tonumber((v1:gsub("[%.x]", ""))), "invalid version: " .. v1)
  end

  function get_busted_handler(min_version, max_version)
    local old_version_var = assert(os.getenv("OLD_KONG_VERSION"), "old version not set")
    local old_version = string.match(old_version_var, "[^/]+$")

    local old_version_num = get_version_num(old_version)
    local min_v_num = min_version and get_version_num(min_version, old_version) or 0
    local max_v_num = max_version and get_version_num(max_version, old_version) or math.huge

    return old_version_num >= min_v_num and old_version_num <= max_v_num and busted.describe or busted.pending
  end
end

return {
  database_type = database_type,
  get_database = get_database,
  create_example_service = create_example_service,
  send_proxy_get_request = send_proxy_get_request,
  start_kong = start_kong,
  stop_kong = helpers.stop_kong,
  admin_client = helpers.admin_client,
  proxy_client = helpers.proxy_client,
  setup = setup,
  old_after_up = old_after_up,
  new_after_up = new_after_up,
  new_after_finish = new_after_finish,
  all_phases = all_phases,
  get_busted_handler = get_busted_handler,
}
