defmodule RevStemmingTest do
  use CouchTestCase

  @moduletag :revs

  @moduledoc """
  This is a port of the rev_stemming.js suite
  """

  @new_limit 5

  @tag :with_db
  test "revs limit update", context do
    db_name = context[:db_name]

    resp = Couch.get("/#{db_name}/_revs_limit")
    assert resp.body == 1000

    create_rev_doc(db_name, "foo", @new_limit + 1)
    resp = Couch.get("/#{db_name}/foo?revs=true")
    assert length(resp.body["_revisions"]["ids"]) == @new_limit + 1

    resp =
      Couch.put("/#{db_name}/_revs_limit",
        body: "#{@new_limit}",
        headers: ["Content-type": "application/json"]
      )

    assert resp.status_code == 200

    create_rev_doc(db_name, "foo", @new_limit + 1)
    resp = Couch.get("/#{db_name}/foo?revs=true")
    assert length(resp.body["_revisions"]["ids"]) == @new_limit
  end

  @tag :with_db
  test "revs limit produces replication conflict ", context do
    db_name = context[:db_name]

    db_name_b = "#{db_name}_b"
    create_db(db_name_b)
    delete_db_on_exit([db_name_b])

    resp =
      Couch.put("/#{db_name}/_revs_limit",
        body: "#{@new_limit}",
        headers: ["Content-type": "application/json"]
      )

    assert resp.status_code == 200

    create_rev_doc(db_name, "foo", @new_limit + 1)
    resp = Couch.get("/#{db_name}/foo?revs=true")
    assert length(resp.body["_revisions"]["ids"]) == @new_limit

    # If you replicate after you make more edits than the limit, you'll
    # cause a spurious edit conflict.
    replicate(db_name, db_name_b)
    resp = Couch.get("/#{db_name_b}/foo?conflicts=true")
    assert not Map.has_key?(resp.body, "_conflicts")

    create_rev_doc(db_name, "foo", @new_limit - 1)

    # one less edit than limit, no conflict
    replicate(db_name, db_name_b)
    resp = Couch.get("/#{db_name_b}/foo?conflicts=true")
    assert not Map.has_key?(resp.body, "_conflicts")
    prev_conflicted_rev = resp.body["_rev"]

    # now we hit the limit
    create_rev_doc(db_name, "foo", @new_limit + 1)

    replicate(db_name, db_name_b)
    resp = Couch.get("/#{db_name_b}/foo?conflicts=true")
    assert Map.has_key?(resp.body, "_conflicts")

    conflicted_rev =
      resp.body["_conflicts"]
      |> Enum.at(0)

    # we have a conflict, but the previous replicated rev is always the losing
    # conflict
    assert conflicted_rev == prev_conflicted_rev
  end

  @tag :with_db
  test "revs limit is kept after compaction", context do
    db_name = context[:db_name]

    create_rev_doc(db_name, "bar", @new_limit + 1)
    resp = Couch.get("/#{db_name}/bar?revs=true")
    assert length(resp.body["_revisions"]["ids"]) == @new_limit + 1

    resp =
      Couch.put("/#{db_name}/_revs_limit",
        body: "#{@new_limit}",
        headers: ["Content-type": "application/json"]
      )

    assert resp.status_code == 200

    # We having already updated bar before setting the limit, so it's still got
    # a long rev history. compact to stem the revs.
    resp = Couch.get("/#{db_name}/bar?revs=true")
    assert length(resp.body["_revisions"]["ids"]) == @new_limit

    compact(db_name)

    # force reload because ETags don't honour compaction
    resp =
      Couch.get("/#{db_name}/bar?revs=true",
        headers: ["if-none-match": "pommes"]
      )

    assert length(resp.body["_revisions"]["ids"]) == @new_limit
  end

  # function to create a doc with multiple revisions
  defp create_rev_doc(db_name, id, num_revs) do
    resp = Couch.get("/#{db_name}/#{id}")

    doc =
      if resp.status_code == 200 do
        resp.body
      else
        %{_id: id, count: 0}
      end

    {:ok, resp} = create_doc(db_name, doc)
    create_rev_doc(db_name, id, num_revs, [Map.put(doc, :_rev, resp.body["rev"])])
  end

  defp create_rev_doc(db_name, id, num_revs, revs) do
    if length(revs) < num_revs do
      doc = %{_id: id, _rev: Enum.at(revs, -1)[:_rev], count: length(revs)}
      {:ok, resp} = create_doc(db_name, doc)

      create_rev_doc(
        db_name,
        id,
        num_revs,
        revs ++ [Map.put(doc, :_rev, resp.body["rev"])]
      )
    else
      revs
    end
  end

  def delete_db_on_exit(db_names) when is_list(db_names) do
    on_exit(fn ->
      Enum.each(db_names, fn name ->
        delete_db(name)
      end)
    end)
  end

end
