defmodule ViewPartitionTest do
  use CouchTestCase
  import PartitionHelpers

  @moduledoc """
  Test Partition functionality for views
  """

  setup_all do
    db_name = random_db_name()
    {:ok, _} = create_db(db_name, query: %{partitioned: true, q: 1})
    on_exit(fn -> delete_db(db_name) end)

    create_partition_docs(db_name)

    map_fun1 = """
      function(doc) {
        if (doc.some) {
          emit(doc.value, doc.some);
        }
      }
    """

    map_fun2 = """
      function(doc) {
        if (doc.group) {
          emit([doc.some, doc.group], 1);
        }
      }
    """

    query = %{:w => 3}

    body = %{
      :docs => [
        %{
          _id: "_design/map",
          views: %{some: %{map: map_fun1}}
        },
        %{
          _id: "_design/map_some",
          views: %{some: %{map: map_fun2}}
        },
        %{
          _id: "_design/partitioned_true",
          views: %{some: %{map: map_fun1}},
          options: %{partitioned: true}
        },
        %{
          _id: "_design/partitioned_false",
          views: %{some: %{map: map_fun1}},
          options: %{partitioned: false}
        },
        %{
          _id: "_design/reduce",
          views: %{some: %{map: map_fun2, reduce: "_count"}}
        },
        %{
          _id: "_design/include_ddocs",
          views: %{some: %{map: map_fun1}},
          options: %{include_design: true}
        }
      ]
    }

    resp = Couch.post("/#{db_name}/_bulk_docs", query: query, body: body)
    Enum.each(resp.body, &assert(&1["ok"]))

    {:ok, [db_name: db_name]}
  end

  def get_reduce_result(resp) do
    %{:body => %{"rows" => rows}} = resp
    rows
  end

  test "query with partitioned:true returns partitioned fields", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/partitioned_true/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert Enum.dedup(partitions) == ["foo"]

    url = "/#{db_name}/_partition/bar/_design/partitioned_true/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert Enum.dedup(partitions) == ["bar"]
  end

  test "default view query returns partitioned fields", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert Enum.dedup(partitions) == ["foo"]

    url = "/#{db_name}/_partition/bar/_design/map/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert Enum.dedup(partitions) == ["bar"]
  end

  test "conflicting partitions in path and query string rejected", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{partition: "bar"})
    assert resp.status_code == 400
    %{:body => %{"reason" => reason}} = resp
    assert Regex.match?(~r/Conflicting value/, reason)
  end

  test "query will return zero results for wrong inputs", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{start_key: "\"foo:12\""})
    assert resp.status_code == 200
    assert Map.get(resp, :body)["rows"] == []
  end

  test "partitioned ddoc cannot be used in global query", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_design/map/_view/some"
    resp = Couch.get(url)
    %{:body => %{"reason" => reason}} = resp
    assert resp.status_code == 400
    assert Regex.match?(~r/mandatory for queries to this view./, reason)
  end

  test "partitioned query cannot be used with global ddoc", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/partitioned_false/_view/some"
    resp = Couch.get(url)
    %{:body => %{"reason" => reason}} = resp
    assert resp.status_code == 400
    assert Regex.match?(~r/is not supported in this design doc/, reason)
  end

  test "view query returns all docs for global query", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_design/partitioned_false/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 100
  end

  test "partition query errors with incorrect partition supplied", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/_bar/_design/map/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 400

    url = "/#{db_name}/_partition//_design/map/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 400
  end

  test "partitioned query works with startkey, endkey range", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{start_key: 12, end_key: 20})
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert length(partitions) == 5
    assert Enum.dedup(partitions) == ["foo"]
  end

  test "partitioned query works with keys", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.post(url, body: %{keys: [2, 4, 6]})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 3
    assert ids == ["foo:2", "foo:4", "foo:6"]
  end

  test "global query works with keys", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_design/partitioned_false/_view/some"
    resp = Couch.post(url, body: %{keys: [2, 4, 6]})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 3
    assert ids == ["foo:2", "foo:4", "foo:6"]
  end

  test "partition query works with limit", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{limit: 5})
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert length(partitions) == 5
    assert Enum.dedup(partitions) == ["foo"]
  end

  test "partition query with descending", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{descending: true, limit: 5})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 5
    assert ids == ["foo:100", "foo:98", "foo:96", "foo:94", "foo:92"]

    resp = Couch.get(url, query: %{descending: false, limit: 5})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 5
    assert ids == ["foo:2", "foo:4", "foo:6", "foo:8", "foo:10"]
  end

  test "partition query with skip", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{skip: 5, limit: 5})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 5
    assert ids == ["foo:12", "foo:14", "foo:16", "foo:18", "foo:20"]
  end

  test "partition query with key", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map/_view/some"
    resp = Couch.get(url, query: %{key: 22})
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 1
    assert ids == ["foo:22"]
  end

  test "partition query with startkey_docid and endkey_docid", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/map_some/_view/some"

    resp =
      Couch.get(
        url,
        query: %{
          startkey: "[\"field\",\"one\"]",
          endkey: "[\"field\",\"one\"]",
          startkey_docid: "foo:12",
          endkey_docid: "foo:30"
        }
      )

    assert resp.status_code == 200
    ids = get_ids(resp)
    assert ids == ["foo:12", "foo:18", "foo:24", "foo:30"]
  end

  test "query with reduce works", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/reduce/_view/some"
    resp = Couch.get(url, query: %{reduce: true, group_level: 1})
    assert resp.status_code == 200
    results = get_reduce_result(resp)
    assert results == [%{"key" => ["field"], "value" => 50}]

    resp = Couch.get(url, query: %{reduce: true, group_level: 2})
    results = get_reduce_result(resp)

    assert results == [
             %{"key" => ["field", "one"], "value" => 16},
             %{"key" => ["field", "two"], "value" => 34}
           ]

    resp = Couch.get(url, query: %{reduce: true, group: true})
    results = get_reduce_result(resp)

    assert results == [
             %{"key" => ["field", "one"], "value" => 16},
             %{"key" => ["field", "two"], "value" => 34}
           ]
  end

  test "partition query can set query limits", context do
    set_config({"query_server_config", "partition_query_limit", "2000"})

    db_name = context[:db_name]
    create_partition_docs(db_name)
    create_partition_ddoc(db_name)

    url = "/#{db_name}/_partition/foo/_design/mrtest/_view/some"

    resp =
      Couch.get(
        url,
        query: %{
          limit: 20
        }
      )

    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 20

    resp = Couch.get(url)
    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 50

    resp =
      Couch.get(
        url,
        query: %{
          limit: 2000
        }
      )

    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 50

    resp =
      Couch.get(
        url,
        query: %{
          limit: 2001
        }
      )

    assert resp.status_code == 400
    %{:body => %{"reason" => reason}} = resp
    assert Regex.match?(~r/Limit is too large/, reason)

    resp =
      Couch.get(
        url,
        query: %{
          limit: 2000,
          skip: 25
        }
      )

    assert resp.status_code == 200
    ids = get_ids(resp)
    assert length(ids) == 25
  end

  test "include_design works correctly", context do
    db_name = context[:db_name]

    url = "/#{db_name}/_partition/foo/_design/include_ddocs/_view/some"
    resp = Couch.get(url)
    assert resp.status_code == 200
    partitions = get_partitions(resp)
    assert length(partitions) == 50
    assert Enum.dedup(partitions) == ["foo"]
  end
end
