local declarative = require "kong.db.declarative"
local ssl_fixtures = require "spec.fixtures.ssl"
local helpers = require "spec.helpers"
local lyaml = require "lyaml"
local crypto = require "kong.plugins.basic-auth.crypto"

for _, strategy in helpers.each_strategy() do
  describe("declarative config #" .. strategy, function()
    local db
    lazy_setup(function()
      local _
      _, db = helpers.get_db_utils(strategy)

      _G.kong.db = db
      assert(helpers.start_kong({
        database   = strategy,
      }))
    end)

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

    local service_def = {
      _tags = ngx.null,
      connect_timeout = 60000,
      created_at = 1549025889,
      host = "example.com",
      id = "3b9c2302-a610-4925-a7b9-25942309335d",
      name = "foo",
      path = ngx.null,
      port = 80,
      protocol = "https",
      read_timeout = 60000,
      retries = 5,
      updated_at = 1549025889,
      write_timeout = 60000,
      tags = { "potato", "carrot" },
    }

    local disabled_service_def = {
      _tags = ngx.null,
      connect_timeout = 60000,
      created_at = 1549025889,
      host = "example.com",
      id = "5c220029-4f4a-48a0-b79b-9eec6f6412c0",
      name = "disabled",
      enabled = false,
      path = ngx.null,
      port = 80,
      protocol = "https",
      read_timeout = 60000,
      retries = 5,
      updated_at = 1549025889,
      write_timeout = 60000,
      tags = { "onions", "celery" },
    }

    local route_def = {
      _tags = ngx.null,
      created_at = 1549025889,
      id = "eb88ccb8-274d-4e7e-b4cb-0d673a4fa93b",
      name = "bar",
      protocols = { "http", "https" },
      methods = ngx.null,
      hosts = { "example.com" },
      paths = ngx.null,
      regex_priority = 0,
      strip_path = true,
      preserve_host = false,
      snis = ngx.null,
      sources = ngx.null,
      destinations = ngx.null,
      service = { id = service_def.id },
    }

    local disabled_route_def = {
      _tags = ngx.null,
      created_at = 1549025889,
      id = "02a6749e-1ae3-4904-b429-894ecd679fc4",
      name = "disabled-bar",
      protocols = { "http", "https" },
      methods = ngx.null,
      hosts = { "example.com" },
      paths = { "/disabled-route" },
      regex_priority = 0,
      strip_path = true,
      preserve_host = false,
      snis = ngx.null,
      sources = ngx.null,
      destinations = ngx.null,
      service = { id = disabled_service_def.id },
    }

    local certificate_def = {
      _tags = ngx.null,
      created_at = 1541088353,
      id = "f6c12564-47c8-48b4-b171-0a0d9dbf7cb0",
      cert  = ssl_fixtures.cert,
      key   = ssl_fixtures.key,
    }

    local sni_def = {
      _tags = ngx.null,
      created_at = 1549689381,
      id = "ae54d23c-9977-4022-8536-8ceac8c0d0f0",
      name = "baz",
      certificate = { id = certificate_def.id },
    }

    local consumer_def = {
      _tags = ngx.null,
      created_at = 1549476023,
      id = "ad06b77c-0d2f-407a-8d6d-07f272a92d6a",
      username = "andru",
      custom_id = "donalds",
    }

    local plugin_def = {
      _tags = ngx.null,
      created_at = 1547047308,
      id = "389ad9bd-b158-4e19-aed7-c9b040f7f312",
      service = { id = service_def.id },
      enabled = true,
      name = "acl",
      config = {
        deny = ngx.null,
        allow = { "*" },
        hide_groups_header = false,
        always_use_authenticated_groups = false,
      }
    }

    local disabled_service_plugin_def = {
      _tags = ngx.null,
      created_at = 1547047309,
      id = "7425f330-cdd1-4f65-a6e9-78d631b3ef72",
      service = { id = disabled_service_def.id },
      enabled = true,
      name = "acl",
      config = {
        deny = ngx.null,
        allow = { "*" },
        hide_groups_header = false,
        always_use_authenticated_groups = false,
      }
    }

    -- plugin is disabled, but attached to enabled service
    local disabled_plugin_def = {
      _tags = ngx.null,
      created_at = 1547047310,
      id = "9d26ae22-dc45-4988-87f6-bd655a676ae6",
      enabled = false,
      name = "key-auth",
      service = { id = service_def.id },
    }

    --[[ FIXME this case is known to cause an issue
    local plugin_with_null_def = {
      _tags = ngx.null,
      created_at = 1547047308,
      id = "57f5af80-8a44-4fbe-bd00-43ab58e2e5a5",
      service = { id = service_def.id },
      enabled = true,
      name = "correlation-id",
      config = {
        header_name = ngx.null,
        generator = "uuid",
        echo_downstream = false,
      }
    }
    --]]

    local acl_def = {
      _tags = ngx.null,
      created_at = 154796740,
      id = "21698f76-e00b-4017-96e5-cc5ece1508a5",
      consumer = { id = consumer_def.id },
      group = "The A Team"
    }

    local basicauth_credential_def = {
      id = "ad06b77c-0d2f-407a-8d6d-07f272a92d9a",
      consumer = {
        id = consumer_def.id,
      },
      username = "james",
      password = "secret",
    }

    local basicauth_hashed_credential_def = {
      id = "caa33a6f-8e6b-4b02-9f55-0e2cffd26fb5",
      consumer = {
        id = consumer_def.id,
      },
      username = "bond",
      password = crypto.hash(consumer_def.id, "MI6"),
    }

    before_each(function()
      db.acls:truncate()
      db.basicauth_credentials:truncate()
      db.plugins:truncate()
      db.routes:truncate()
      db.services:truncate()
      db.snis:truncate()
      db.certificates:truncate()
      db.consumers:truncate()

      assert(declarative.load_into_db({
        snis = { [sni_def.id] = sni_def },
        certificates = { [certificate_def.id] = certificate_def },
        routes = {
          [route_def.id] = route_def,
          [disabled_route_def.id] = disabled_route_def,
        },
        services = {
          [service_def.id] = service_def,
          [disabled_service_def.id] = disabled_service_def,
       },
        consumers = { [consumer_def.id] = consumer_def },
        plugins = {
          [plugin_def.id] = plugin_def,
          [disabled_service_plugin_def.id] = disabled_service_plugin_def,
          [disabled_plugin_def.id] = disabled_plugin_def,
        -- [plugin_with_null_def.id] = plugin_with_null_def,
        },
        acls = { [acl_def.id] = acl_def  },
        basicauth_credentials = { [basicauth_credential_def.id] = basicauth_credential_def },
      }, { _transform = true }))

      -- import without performing transformations
      assert(declarative.load_into_db({
        basicauth_credentials = { [basicauth_hashed_credential_def.id] = basicauth_hashed_credential_def },
      }, { _transform = false }))
    end)

    describe("load_into_db", function()
      it("imports base and custom entities with associations", function()
        local sni = assert(db.snis:select_by_name("baz"))
        assert.equals(sni_def.id, sni.id)
        assert.equals(certificate_def.id, sni.certificate.id)

        local cert = assert(db.certificates:select(certificate_def))
        assert.equals(certificate_def.id, cert.id)
        assert.same(ssl_fixtures.key, cert.key)
        assert.same(ssl_fixtures.cert, cert.cert)

        local service = assert(db.services:select_by_name("foo"))
        assert.equals(service_def.id, service.id)
        assert.equals("example.com", service.host)
        assert.equals("https", service.protocol)

        local route = assert(db.routes:select_by_name("bar"))
        assert.equals(route_def.id, route.id)
        assert.equals("example.com", route.hosts[1])
        assert.same({ "http", "https" }, route.protocols)
        assert.equals(service_def.id, route.service.id)

        local consumer = assert(db.consumers:select_by_username("andru"))
        assert.equals(consumer_def.id, consumer.id)
        assert.equals("andru", consumer_def.username)
        assert.equals("donalds", consumer_def.custom_id)

        local plugin = assert(db.plugins:select(plugin_def, { nulls = true }))
        assert.equals(plugin_def.id, plugin.id)
        assert.equals(service.id, plugin.service.id)
        assert.equals("acl", plugin.name)
        assert.same(plugin_def.config, plugin.config)

        local acl = assert(db.acls:select(acl_def))
        assert.equals(consumer_def.id, acl.consumer.id)
        assert.equals("The A Team", acl.group)

        local basicauth_credential = assert(db.basicauth_credentials:select(basicauth_credential_def))
        assert.equals(basicauth_credential_def.id, basicauth_credential.id)
        assert.equals(consumer.id, basicauth_credential.consumer.id)
        assert.equals("james", basicauth_credential.username)
        assert.equals(crypto.hash(consumer.id, "secret"), basicauth_credential.password)

        local basicauth_hashed_credential = assert(db.basicauth_credentials:select(basicauth_hashed_credential_def))
        assert.equals(basicauth_hashed_credential_def.id, basicauth_hashed_credential.id)
        assert.equals(consumer.id, basicauth_hashed_credential.consumer.id)
        assert.equals("bond", basicauth_hashed_credential.username)
        assert.equals(basicauth_hashed_credential_def.password, basicauth_hashed_credential.password)
      end)
    end)

    describe("export_from_db", function()
      it("exports base and custom entities with associations", function()
        local fake_file = {
          buffer = {},
          write = function(self, str)
            self.buffer[#self.buffer + 1] = str
          end,
        }

        assert(declarative.export_from_db(fake_file))

        local exported_str = table.concat(fake_file.buffer)
        local yaml = lyaml.load(exported_str)

        -- ensure tags & basicauth_credentials are not being exported
        local toplevel_keys = {}
        for k in pairs(yaml) do
          toplevel_keys[#toplevel_keys + 1] = k
        end
        table.sort(toplevel_keys)
        assert.same({
          "_format_version",
          "_transform",
          "acls",
          "basicauth_credentials",
          "certificates",
          "consumers",
          "parameters",
          "plugins",
          "routes",
          "services",
          "snis"
        }, toplevel_keys)

        assert.equals("3.0", yaml._format_version)
        assert.equals(false, yaml._transform)

        assert.equals(1, #yaml.snis)
        local sni = assert(yaml.snis[1])
        assert.equals(sni_def.id, sni.id)
        assert.equals(sni_def.name, sni.name)
        assert.equals(certificate_def.id, sni.certificate)

        assert.equals(1, #yaml.certificates)
        local cert = assert(yaml.certificates[1])
        assert.equals(certificate_def.id, cert.id)
        assert.equals(ssl_fixtures.key, cert.key)
        assert.equals(ssl_fixtures.cert, cert.cert)

        assert.equals(2, #yaml.services)
        local service = assert(yaml.services[1])
        assert.equals(service_def.id, service.id)
        assert.equals("example.com", service.host)
        assert.equals("https", service.protocol)
        table.sort(service.tags)
        assert.same({"carrot", "potato"}, service.tags)

        -- expect disabled services and associated route and plugins to exist
        local disabled_service = assert(yaml.services[2])
        assert.equals(disabled_service_def.id, disabled_service.id)
        assert.equals("example.com", disabled_service.host)
        assert.equals("https", disabled_service.protocol)
        table.sort(disabled_service.tags)
        assert.same({"celery", "onions"}, disabled_service.tags)

        assert.equals(2, #yaml.routes)
        local route = assert(yaml.routes[2])
        assert.equals(route_def.id, route.id)
        assert.equals("bar", route.name)
        assert.equals("example.com", route.hosts[1])
        assert.same({ "http", "https" }, route.protocols)
        assert.equals(service_def.id, route.service)

        local disabled_route = assert(yaml.routes[1])
        assert.equals(disabled_route_def.id, disabled_route.id)
        assert.equals("example.com", disabled_route.hosts[1])
        assert.same({ "http", "https" }, disabled_route.protocols)
        assert.equals(disabled_service_def.id, disabled_route.service)

        assert.equals(1, #yaml.consumers)
        local consumer = assert(yaml.consumers[1])
        assert.equals(consumer_def.id, consumer.id)
        assert.equals("andru", consumer_def.username)
        assert.equals("donalds", consumer_def.custom_id)

        assert.equals(3, #yaml.plugins)
        local plugin = assert(yaml.plugins[1])
        assert.equals(plugin_def.id, plugin.id)
        assert.equals(service.id, plugin.service)
        assert.equals("acl", plugin.name)

        local service_disabled_plugin = assert(yaml.plugins[2])
        assert.equals(disabled_service_plugin_def.id, service_disabled_plugin.id)
        assert.equals(disabled_service_def.id, service_disabled_plugin.service)
        assert.equals("acl", service_disabled_plugin.name)

        local disabled_plugin = assert(yaml.plugins[3])
        assert.equals(disabled_plugin_def.id, disabled_plugin.id)
        assert.equals(service_def.id, disabled_plugin.service)
        assert.equals("key-auth", disabled_plugin.name)

        -- lyaml.load above returns null as its own format
        assert(plugin.config.deny == lyaml.null)
        plugin.config.deny = ngx.null

        assert.same(plugin_def.config, plugin.config)

        --[[ FIXME this case is known to cause an issue
        local plugin_with_null = assert(db.plugins:select(plugin_with_null_def, { nulls = true }))
        assert.equals(plugin_with_null_def.id, plugin_with_null.id)
        assert.equals(service.id, plugin_with_null.service.id)
        assert.equals("correlation-id", plugin_with_null.name)
        assert.same(plugin_with_null_def.config, plugin_with_null.config
        --]]

        assert.equals(1, #yaml.acls)
        local acl = assert(yaml.acls[1])
        assert.equals(consumer_def.id, acl.consumer)
        assert.equals("The A Team", acl.group)

        assert.equals(2, #yaml.basicauth_credentials)
        table.sort(yaml.basicauth_credentials, function(a, b)
          return a.username > b.username
        end)

        local bac1 = assert(yaml.basicauth_credentials[1])
        assert.equals(consumer_def.id, bac1.consumer)
        assert.equals("james", bac1.username)
        assert.equals(crypto.hash(consumer_def.id, "secret"), bac1.password)

        local bac2 = assert(yaml.basicauth_credentials[2])
        assert.equals(consumer_def.id, bac2.consumer)
        assert.equals("bond", bac2.username)
        assert.equals(basicauth_hashed_credential_def.password, bac2.password)
      end)

      it('exports from db without disabled services, and associated routes and plugins, skip_disabled_services=true', function ()
        local fake_file = {
          buffer = {},
          write = function(self, str)
            self.buffer[#self.buffer + 1] = str
          end,
        }

        assert(declarative.export_from_db(fake_file, true, true))

        local exported_str = table.concat(fake_file.buffer)
        local yaml = lyaml.load(exported_str)

        -- ensure tags & basicauth_credentials are not being exported
        local toplevel_keys = {}
        for k in pairs(yaml) do
          toplevel_keys[#toplevel_keys + 1] = k
        end
        table.sort(toplevel_keys)
        assert.same({
          "_format_version",
          "_transform",
          "acls",
          "basicauth_credentials",
          "certificates",
          "consumers",
          "parameters",
          "plugins",
          "routes",
          "services",
          "snis"
        }, toplevel_keys)

        assert.equals("3.0", yaml._format_version)
        assert.equals(false, yaml._transform)

        assert.equals(1, #yaml.snis)
        local sni = assert(yaml.snis[1])
        assert.equals(sni_def.id, sni.id)
        assert.equals(sni_def.name, sni.name)
        assert.equals(certificate_def.id, sni.certificate)

        assert.equals(1, #yaml.certificates)
        local cert = assert(yaml.certificates[1])
        assert.equals(certificate_def.id, cert.id)
        assert.equals(ssl_fixtures.key, cert.key)
        assert.equals(ssl_fixtures.cert, cert.cert)

        -- expect disabled services and associated route and plugins to not exist
        assert.equals(1, #yaml.services)
        local service = assert(yaml.services[1])
        assert.equals(service_def.id, service.id)
        assert.equals("example.com", service.host)
        assert.equals("https", service.protocol)
        table.sort(service.tags)
        assert.same({"carrot", "potato"}, service.tags)

        assert.equals(1, #yaml.routes)
        local route = assert(yaml.routes[1])
        assert.equals(route_def.id, route.id)
        assert.equals("example.com", route.hosts[1])
        assert.same({ "http", "https" }, route.protocols)
        assert.equals(service_def.id, route.service)

        assert.equals(1, #yaml.consumers)
        local consumer = assert(yaml.consumers[1])
        assert.equals(consumer_def.id, consumer.id)
        assert.equals("andru", consumer_def.username)
        assert.equals("donalds", consumer_def.custom_id)

        assert.equals(1, #yaml.plugins)
        local plugin = assert(yaml.plugins[1])
        assert.equals(plugin_def.id, plugin.id)
        assert.equals(service.id, plugin.service)
        assert.equals("acl", plugin.name)

        -- lyaml.load above returns null as its own format
        assert(plugin.config.deny == lyaml.null)
        plugin.config.deny = ngx.null

        assert.same(plugin_def.config, plugin.config)

        --[[ FIXME this case is known to cause an issue
        local plugin_with_null = assert(db.plugins:select(plugin_with_null_def, { nulls = true }))
        assert.equals(plugin_with_null_def.id, plugin_with_null.id)
        assert.equals(service.id, plugin_with_null.service.id)
        assert.equals("correlation-id", plugin_with_null.name)
        assert.same(plugin_with_null_def.config, plugin_with_null.config
        --]]

        assert.equals(1, #yaml.acls)
        local acl = assert(yaml.acls[1])
        assert.equals(consumer_def.id, acl.consumer)
        assert.equals("The A Team", acl.group)

        assert.equals(2, #yaml.basicauth_credentials)
        table.sort(yaml.basicauth_credentials, function(a, b)
          return a.username > b.username
        end)

        local bac1 = assert(yaml.basicauth_credentials[1])
        assert.equals(consumer_def.id, bac1.consumer)
        assert.equals("james", bac1.username)
        assert.equals(crypto.hash(consumer_def.id, "secret"), bac1.password)

        local bac2 = assert(yaml.basicauth_credentials[2])
        assert.equals(consumer_def.id, bac2.consumer)
        assert.equals("bond", bac2.username)
        assert.equals(basicauth_hashed_credential_def.password, bac2.password)
      end)
    end)
  end)
end
