defmodule ConfigTest do
  use CouchTestCase

  @moduletag :config

  @moduledoc """
  Test CouchDB config API
  This is a port of the config.js suite
  """

  setup do
    # TODO: switch this to _local when that's landed
    config_url = "/_node/node1@127.0.0.1/_config"
    resp = Couch.get(config_url)
    assert resp.status_code == 200
    {:ok, config: resp.body, config_url: config_url}
  end

  def set_config(context, section, key, val) do
    set_config(context, section, key, val, 200)
  end

  def set_config(context, section, key, val, status_assert) do
    url = "#{context[:config_url]}/#{section}/#{key}"
    headers = ["X-Couch-Persist": "false"]
    resp = Couch.put(url, headers: headers, body: :jiffy.encode(val))

    if status_assert do
      assert resp.status_code == status_assert
    end

    resp.body
  end

  def get_config(context, section) do
    get_config(context, section, nil, 200)
  end

  def get_config(context, section, key) do
    get_config(context, section, key, 200)
  end

  def get_config(context, section, key, status_assert) do
    url =
      if key do
        "#{context[:config_url]}/#{section}/#{key}"
      else
        "#{context[:config_url]}/#{section}"
      end

    resp = Couch.get(url)

    if status_assert do
      assert resp.status_code == status_assert
    end

    resp.body
  end

  def delete_config(context, section, key) do
    delete_config(context, section, key, 200)
  end

  def delete_config(context, section, key, status_assert) do
    url = "#{context[:config_url]}/#{section}/#{key}"
    resp = Couch.delete(url)

    if status_assert do
      assert resp.status_code == status_assert
    end
  end

  # TODO: port sever_port tests from config.js
  @tag :pending
  test "CouchDB respects configured protocols"

  test "Standard config options are present", context do
    assert context[:config]["couchdb"]["database_dir"]
    assert context[:config]["chttpd"]["port"]
  end

  test "Settings can be altered with undefined whitelist allowing any change", context do
    refute context["config"]["chttpd"]["config_whitelist"], "Default whitelist is empty"
    set_config(context, "test", "foo", "bar")
    assert get_config(context, "test")["foo"] == "bar"
    assert get_config(context, "test", "foo") == "bar"
  end

  test "Server-side password hashing, and raw updates disabling that", context do
    plain_pass = "s3cret"
    set_config(context, "admins", "administrator", plain_pass)
    assert Couch.login("administrator", plain_pass)
    hash_pass = get_config(context, "admins", "administrator")

    assert Regex.match?(~r/^-pbkdf2(:[a-z0-9]+)?-/, hash_pass) or
             Regex.match?(~r/^-hashed-/, hash_pass)

    delete_config(context, "admins", "administrator")
    assert Couch.delete("/_session").body["ok"]
  end

  @tag :pending
  test "PORT `BUGGED` ?raw tests from config.js"

  test "Non-term whitelist values allow further modification of the whitelist", context do
    val = "!This is an invalid Erlang term!"
    set_config(context, "chttpd", "config_whitelist", val)
    assert val == get_config(context, "chttpd", "config_whitelist")
    delete_config(context, "chttpd", "config_whitelist")
  end

  test "Non-list whitelist values allow further modification of the whitelist", context do
    val = "{[yes, a_valid_erlang_term, but_unfortunately, not_a_list]}"
    set_config(context, "chttpd", "config_whitelist", val)
    assert val == get_config(context, "chttpd", "config_whitelist")
    delete_config(context, "chttpd", "config_whitelist")
  end

  test "Keys not in the whitelist may not be modified", context do
    val = "[{chttpd,config_whitelist}, {test,foo}]"
    set_config(context, "chttpd", "config_whitelist", val)
    assert val == get_config(context, "chttpd", "config_whitelist")
    set_config(context, "test", "foo", "PUT to whitelisted config variable")
    delete_config(context, "test", "foo")
  end

  test "Non-2-tuples in the whitelist are ignored", context do
    val =
      "[{chttpd,config_whitelist}, these, {are}, {nOt, 2, tuples}, [so], [they, will], [all, become, noops], {test,foo}]"

    set_config(context, "chttpd", "config_whitelist", val)
    assert val == get_config(context, "chttpd", "config_whitelist")
    set_config(context, "test", "foo", "PUT to whitelisted config variable")
    delete_config(context, "test", "foo")
  end

  test "Atoms, binaries, and strings suffice as whitelist sections and keys.", context do
    vals = ["{test,foo}", "{\"test\",\"foo\"}", "{<<\"test\">>,<<\"foo\">>}"]

    Enum.each(vals, fn pair ->
      set_config(
        context,
        "chttpd",
        "config_whitelist",
        "[{chttpd,config_whitelist}, #{pair}"
      )

      pair_format =
        case String.at(pair, 1) do
          "t" -> "tuple"
          "\"" -> "string"
          "<" -> "binary"
        end

      set_config(context, "test", "foo", "PUT with #{pair_format}")
      delete_config(context, "test", "foo")
    end)

    delete_config(context, "chttpd", "config_whitelist")
  end

  test "Blacklist is functional", context do
    sections = [
      "daemons",
      "external",
      "httpd_design_handlers",
      "httpd_db_handlers",
      "native_query_servers",
      "os_daemons",
      "query_servers"
    ]

    Enum.each(sections, fn section ->
      set_config(context, section, "wohali", "rules", 403)
    end)
  end

  test "Reload config", context do
    url = "#{context[:config_url]}/_reload"
    resp = Couch.post(url)

    assert resp.status_code == 200
  end

  # Those are negative test cases.  The positive cases are implicitly
  # tested by other ones.
  test "Only JSON strings are accepted", context do
    url = "#{context[:config_url]}/a/b"
    values = ["true", "11", "{}", "{\"testing\": [1, 2, 3]}"]

    Enum.each(values, fn value ->
      resp = Couch.put(url, body: value)
      assert resp.status_code == 400
    end)
  end
end
