defmodule AuthCacheTest do
  use CouchTestCase

  @moduletag :authentication

  @tag :pending
  @tag :with_db
  test "auth cache management", context do
    db_name = context[:db_name]

    server_config = [
      %{
        :section => "chttpd_auth",
        :key => "authentication_db",
        :value => db_name
      },
      %{
        :section => "chttpd_auth",
        :key => "auth_cache_size",
        :value => "3"
      },
      %{
        :section => "httpd",
        :key => "authentication_handlers",
        :value => "{couch_httpd_auth, default_authentication_handler}"
      },
      %{
        :section => "chttpd_auth",
        :key => "secret",
        :value => generate_secret(64)
      }
    ]

    run_on_modified_server(server_config, fn -> test_fun(db_name) end)
  end

  defp generate_secret(len) do
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    |> String.splitter("", trim: true)
    |> Enum.take_random(len)
    |> Enum.join("")
  end

  defp hits() do
    hits = request_stats(["couchdb", "auth_cache_hits"], true)
    hits["value"] || 0
  end

  defp misses() do
    misses = request_stats(["couchdb", "auth_cache_misses"], true)
    misses["value"] || 0
  end

  defp logout(session) do
    assert Couch.Session.logout(session).body["ok"]
  end

  defp login_fail(user, password) do
    resp = Couch.login(user, password, :fail)
    assert resp.error, "Login error is expected."
  end

  defp login(user, password) do
    sess = Couch.login(user, password)
    assert sess.cookie, "Login correct is expected"
    sess
  end

  defp assert_cache(event, user, password, expect \\ :expect_login_success) do
    hits_before = hits()
    misses_before = misses()

    session =
      case expect do
        :expect_login_success -> login(user, password)
        :expect_login_fail -> login_fail(user, password)
        _ -> assert false
      end

    hits_after = hits()
    misses_after = misses()

    if expect == :expect_success do
      logout(session)
    end

    case event do
      :expect_miss ->
        assert misses_after == misses_before + 1,
               "Cache miss is expected for #{user} after login"

        assert hits_after == hits_before,
               "No cache hit is expected for #{user} after login"

      :expect_hit ->
        assert misses_after == misses_before,
               "No cache miss is expected for #{user} after login"

        assert hits_after == hits_before + 1,
               "Cache hit is expected for #{user} after login"

      _ ->
        assert false
    end
  end

  def save_doc(db_name, body) do
    resp = Couch.put("/#{db_name}/#{body["_id"]}", body: body)
    assert resp.status_code in [201, 202]
    assert resp.body["ok"]
    Map.put(body, "_rev", resp.body["rev"])
  end

  def delete_doc(db_name, body) do
    resp = Couch.delete("/#{db_name}/#{body["_id"]}", query: [rev: body["_rev"]])
    assert resp.status_code in [200, 202]
    assert resp.body["ok"]
    {:ok, resp}
  end

  defp test_fun(db_name) do
    fdmanana =
      prepare_user_doc([
        {:name, "fdmanana"},
        {:password, "qwerty"},
        {:roles, ["dev"]}
      ])

    {:ok, resp} = create_doc(db_name, fdmanana)
    fdmanana = Map.put(fdmanana, "_rev", resp.body["rev"])

    chris =
      prepare_user_doc([
        {:name, "chris"},
        {:password, "the_god_father"},
        {:roles, ["dev", "mafia", "white_costume"]}
      ])

    create_doc(db_name, chris)

    joe =
      prepare_user_doc([
        {:name, "joe"},
        {:password, "functional"},
        {:roles, ["erlnager"]}
      ])

    create_doc(db_name, joe)

    johndoe =
      prepare_user_doc([
        {:name, "johndoe"},
        {:password, "123456"},
        {:roles, ["user"]}
      ])

    create_doc(db_name, johndoe)

    assert_cache(:expect_miss, "fdmanana", "qwerty")
    assert_cache(:expect_hit, "fdmanana", "qwerty")
    assert_cache(:expect_miss, "chris", "the_god_father")
    assert_cache(:expect_miss, "joe", "functional")
    assert_cache(:expect_miss, "johndoe", "123456")

    # It's a MRU cache, joe was removed from cache to add johndoe
    # BUGGED assert_cache(:expect_miss, "joe", "functional")

    assert_cache(:expect_hit, "fdmanana", "qwerty")

    fdmanana = Map.replace!(fdmanana, "password", "foobar")
    fdmanana = save_doc(db_name, fdmanana)

    # Cache was refreshed
    # BUGGED
    # assert_cache(:expect_hit, "fdmanana", "qwerty", :expect_login_fail)
    # assert_cache(:expect_hit, "fdmanana", "foobar")

    # and yet another update
    fdmanana = Map.replace!(fdmanana, "password", "javascript")
    fdmanana = save_doc(db_name, fdmanana)

    # Cache was refreshed
    # BUGGED
    # assert_cache(:expect_hit, "fdmanana", "foobar", :expect_login_fail)
    # assert_cache(:expect_hit, "fdmanana", "javascript")

    delete_doc(db_name, fdmanana)

    assert_cache(:expect_hit, "fdmanana", "javascript", :expect_login_fail)

    # login, compact authentication DB, login again and verify that
    # there was a cache hit
    assert_cache(:expect_hit, "johndoe", "123456")
    compact(db_name)
    assert_cache(:expect_hit, "johndoe", "123456")
  end
end
