local util = require("resty.acme.util")

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

local pkey = require("resty.openssl.pkey")
local x509 = require("resty.openssl.x509")

local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy

local client

local function new_cert_key_pair(expire)
  local key = pkey.new(nil, 'EC', 'prime256v1')
  local crt = x509.new()
  crt:set_pubkey(key)
  crt:set_version(3)
  if expire then
    crt:set_not_after(expire)
  end
  crt:sign(key)
  return key:to_PEM("private"), crt:to_PEM()
end

local strategies = {}
for _, strategy in helpers.each_strategy() do
  table.insert(strategies, strategy)
end
table.insert(strategies, "off")

local proper_config = {
  account_email = "someone@somedomain.com",
  api_uri = "http://api.someacme.org",
  storage = "shm",
  storage_config = {
    shm = { shm_name = "kong" },
  },
  renew_threshold_days = 30,
}

for _, strategy in ipairs(strategies) do
  local _, db

  lazy_setup(function()
    _, db = helpers.get_db_utils(strategy, {
      "acme_storage"
    }, { "acme", })

    client = require("kong.plugins.acme.client")

    local account_name = client._account_name(proper_config)

    local fake_cache = {
      [account_name] = {
        key = util.create_pkey(),
        kid = "fake kid url",
      },
    }

    kong.cache = {
      get = function(_, _, _, f, _, k)
        return fake_cache[k]
      end
    }

    db.acme_storage:insert {
      key = account_name,
      value = fake_cache[account_name],
    }

  end)

  describe("Plugin: acme (client.new) [#" .. strategy .. "]", function()
    it("rejects invalid account config", function()
      local c, err = client.new({
        storage = "shm",
        storage_config = {
          shm = nil,
        },
        api_uri = proper_config.api_uri,
        account_email = "notme@exmaple.com",
      })
      assert.is_nil(c)
      assert.equal(err, "shm is not defined in plugin storage config")
    end)

    it("creates acme client properly", function()
      local c, err = client.new(proper_config)
      assert.is_nil(err)
      assert.not_nil(c)
    end)
  end)
end

for _, strategy in ipairs(strategies) do
  local account_name, account_key
  local c, config, db

  local KEY_ID = "123"
  local KEY_SET_NAME = "key_set_foo"

  local pem_pub, pem_priv

  lazy_setup(function()
    pem_pub, pem_priv = helpers.generate_keys("PEM")
    client = require("kong.plugins.acme.client")
    account_name = client._account_name(proper_config)
  end)

  describe("Plugin: acme (client.create_account) [#" .. strategy .. "]", function()
    describe("create with preconfigured account_key with key_set", function()
      lazy_setup(function()
        account_key = {key_id = KEY_ID, key_set = KEY_SET_NAME}
        config = cycle_aware_deep_copy(proper_config)
        config.account_key = account_key
        c = client.new(config)

        _, db = helpers.get_db_utils(strategy ~= "off" and strategy or nil, {"keys", "key_sets"})

        local ks, err = assert(db.key_sets:insert({name = KEY_SET_NAME}))
        assert.is_nil(err)

        local k, err = db.keys:insert({
          name = "Test PEM",
          pem = {
            private_key = pem_priv,
            public_key = pem_pub
          },
          set = ks,
          kid = KEY_ID
        })
        assert(k)
        assert.is_nil(err)
      end)

      lazy_teardown(function()
        c.storage:delete(account_name)
      end)

      -- The first call should result in the account key being persisted.
      it("persists account", function()
        local err = client._create_account(config)
        assert.is_nil(err)

        local account, err = c.storage:get(account_name)
        assert.is_nil(err)
        assert.not_nil(account)

        local account_data = cjson.decode(account)
        assert.equal(account_data.key, pem_priv)
      end)

      -- The second call should be a nop because the key is found in the db.
      -- Validate that the second call does not result in the key being changed.
      it("skips persisting existing account", function()
        local err = client._create_account(config)
        assert.is_nil(err)

        local account, err = c.storage:get(account_name)
        assert.is_nil(err)
        assert.not_nil(account)

        local account_data = cjson.decode(account)
        assert.equal(account_data.key, pem_priv)
      end)
    end)

    describe("create with preconfigured account_key without key_set", function()
      lazy_setup(function()
        account_key = {key_id = KEY_ID}
        config = cycle_aware_deep_copy(proper_config)
        config.account_key = account_key
        c = client.new(config)

        _, db = helpers.get_db_utils(strategy ~= "off" and strategy or nil, {"keys", "key_sets"})

        local k, err = db.keys:insert({
          name = "Test PEM",
          pem = {
            private_key = pem_priv,
            public_key = pem_pub
          },
          kid = KEY_ID
        })
        assert(k)
        assert.is_nil(err)
      end)

      lazy_teardown(function()
        c.storage:delete(account_name)
      end)

      -- The first call should result in the account key being persisted.
      it("persists account", function()
        local err = client._create_account(config)
        assert.is_nil(err)

        local account, err = c.storage:get(account_name)
        assert.is_nil(err)
        assert.not_nil(account)

        local account_data = cjson.decode(account)
        assert.equal(account_data.key, pem_priv)
      end)
    end)

    describe("create with generated account_key", function()
      local i = 1
      local account_keys = {}

      lazy_setup(function()
        config = cycle_aware_deep_copy(proper_config)
        c = client.new(config)

        account_keys[1] = util.create_pkey()
        account_keys[2] = util.create_pkey()

        util.create_pkey = function(size, type)
          local key = account_keys[i]
          i = i + 1
          return key
        end
      end)

      lazy_teardown(function()
        c.storage:delete(account_name)
      end)

      -- The first call should result in a key being generated and the account
      -- should then be persisted.
      it("persists account", function()
        local err = client._create_account(config)
        assert.is_nil(err)

        local account, err = c.storage:get(account_name)
        assert.is_nil(err)
        assert.not_nil(account)

        local account_data = cjson.decode(account)
        assert.equal(account_data.key, account_keys[1])
      end)

      -- The second call should be a nop because the key is found in the db.
      it("skip persisting existing account", function()
        local err = client._create_account(config)
        assert.is_nil(err)

        local account, err = c.storage:get(account_name)
        assert.is_nil(err)
        assert.not_nil(account)

        local account_data = cjson.decode(account)
        assert.equal(account_data.key, account_keys[1])
      end)
    end)
  end)
end

for _, strategy in helpers.each_strategy() do
  describe("Plugin: acme (client.save) [#" .. strategy .. "]", function()
    local bp, db
    local cert, sni
    local host = "test1.test"

    lazy_setup(function()
      bp, db = helpers.get_db_utils(strategy, {
        "certificates",
        "snis",
      }, { "acme", })

      local key, crt = new_cert_key_pair()
      cert = bp.certificates:insert {
        cert = crt,
        key = key,
        tags = { "managed-by-acme" },
      }

      sni = bp.snis:insert {
        name = host,
        certificate = cert,
        tags = { "managed-by-acme" },
      }

    end)

    describe("creates new cert", function()
      local key, crt = new_cert_key_pair()
      local new_sni, new_cert, err
      local new_host = "test2.test"

      it("returns no error", function()
        err = client._save_dao(new_host, key, crt)
        assert.is_nil(err)
      end)

      it("create new sni", function()
        new_sni, err = db.snis:select_by_name(new_host)
        assert.is_nil(err)
        assert.not_nil(new_sni.certificate.id)
      end)

      it("create new certificate", function()
        new_cert, err = db.certificates:select(new_sni.certificate)
        assert.is_nil(err)
        assert.same(new_cert.key, key)
        assert.same(new_cert.cert, crt)
      end)
    end)

    describe("update", function()
      local key, crt = new_cert_key_pair()
      local new_sni, new_cert, err

      it("returns no error", function()
        err = client._save_dao(host, key, crt)
        assert.is_nil(err)
      end)

      it("updates existing sni", function()
        new_sni, err = db.snis:select_by_name(host)
        assert.is_nil(err)
        assert.same(new_sni.id, sni.id)
        assert.not_nil(new_sni.certificate.id)
        assert.not_same(new_sni.certificate.id, sni.certificate.id)
      end)

      it("creates new certificate", function()
        new_cert, err = db.certificates:select(new_sni.certificate)
        assert.is_nil(err)
        assert.same(new_cert.key, key)
        assert.same(new_cert.cert, crt)
      end)

      it("deletes old certificate", function()
        new_cert, err = db.certificates:select(cert)
        assert.is_nil(err)
        assert.is_nil(new_cert)
      end)
    end)
  end)
end

for _, strategy in ipairs({"off"}) do
  describe("Plugin: acme (client.renew) [#" .. strategy .. "]", function()
    local bp
    local cert
    local host = "test1.test"
    local host_not_expired = "test2.test"
    -- make it due for renewal
    local key, crt = new_cert_key_pair(ngx.time() - 23333)
    -- make it not due for renewal
    local key_not_expired, crt_not_expired = new_cert_key_pair(ngx.time() + 365 * 86400)

    lazy_setup(function()
      bp, _ = helpers.get_db_utils(strategy, {
        "certificates",
        "snis",
      }, { "acme", })

      cert = bp.certificates:insert {
        cert = crt,
        key = key,
        tags = { "managed-by-acme" },
      }

      bp.snis:insert {
        name = host,
        certificate = cert,
        tags = { "managed-by-acme" },
      }

      cert = bp.certificates:insert {
        cert = crt_not_expired,
        key = key_not_expired,
        tags = { "managed-by-acme" },
      }

      bp.snis:insert {
        name = host_not_expired,
        certificate = cert,
        tags = { "managed-by-acme" },
      }

      client = require("kong.plugins.acme.client")
      -- hack in unit test mode
      client._set_is_dbless(strategy == "off")
    end)

    describe("", function()
      it("deletes renew config is cert is deleted", function()
        local c, err = client.new(proper_config)
        assert.is_nil(err)

        local host = "dne.konghq.com"
        -- write a dummy renew config
        err = c.storage:set(client._renew_key_prefix .. host, cjson.encode({
          host = host,
          -- make it due for renewal
          expire_at = ngx.time() - 23333,
        }))
        assert.is_nil(err)
        -- do the renewal
        err = client._renew_certificate_storage(proper_config)
        assert.is_nil(err)
        -- the dummy config should now be deleted
        local v, err = c.storage:get(client._renew_key_prefix .. host)
        assert.is_nil(err)
        assert.is_nil(v)
      end)

      it("renews a certificate when it's expired", function()
        local c, err = client.new(proper_config)

        assert.is_nil(err)
        if strategy == "off" then
          err = c.storage:set(client._certkey_key_prefix .. host, cjson.encode({
            cert = crt,
            key = key,
          }))
          assert.is_nil(err)
        end

        local certkey, err = client.load_certkey(proper_config, host)
        assert.is_nil(err)
        assert.not_nil(certkey)
        assert.not_nil(certkey.cert)
        assert.not_nil(certkey.key)
        -- check renewal
        local renew, err = client._check_expire(certkey.cert, 30 * 86400)
        assert.is_nil(err)
        assert.is_truthy(renew)
      end)

      it("does not renew a certificate when it's not expired", function()
        local c, err = client.new(proper_config)

        assert.is_nil(err)
        if strategy == "off" then
          err = c.storage:set(client._certkey_key_prefix .. host_not_expired, cjson.encode({
            cert = crt_not_expired,
            key = key_not_expired,
          }))
          assert.is_nil(err)
        end

        local certkey, err = client.load_certkey(proper_config, host_not_expired)
        assert.is_nil(err)
        assert.not_nil(certkey)
        assert.not_nil(certkey.cert)
        assert.not_nil(certkey.key)
        -- check renewal
        local renew, err = client._check_expire(certkey.cert, 30 * 86400)
        assert.is_nil(err)
        assert.is_falsy(renew)
      end)

      it("calling handler.renew with a false argument should be successful", function()
        local handler = require("kong.plugins.acme.handler")
        handler:configure({{domains = {"example.com"}}})

        local original = client.renew_certificate
        client.renew_certificate = function (config)
          print("mock renew_certificate")
        end
        handler.renew(false)
        client.renew_certificate = original
      end)
    end)

  end)
end
