defmodule DesignDocsTest do
  use CouchTestCase

  @moduletag :design_docs

  @design_doc %{
    _id: "_design/test",
    language: "javascript",
    autoupdate: false,
    whatever: %{
      stringzone: "exports.string = 'plankton';",
      commonjs: %{
        whynot: """
        exports.test = require('../stringzone');
        exports.foo = require('whatever/stringzone');
        """,
        upper: """
        exports.testing = require('./whynot').test.string.toUpperCase()+
        module.id+require('./whynot').foo.string
        """,
        circular_one: "require('./circular_two'); exports.name = 'One';",
        circular_two: "require('./circular_one'); exports.name = 'Two';"
      },
      # paths relative to parent
      idtest1: %{
        a: %{
          b: %{d: "module.exports = require('../c/e').id;"},
          c: %{e: "exports.id = module.id;"}
        }
      },
      # multiple paths relative to parent
      idtest2: %{
        a: %{
          b: %{d: "module.exports = require('../../a/c/e').id;"},
          c: %{e: "exports.id = module.id;"}
        }
      },
      # paths relative to module
      idtest3: %{
        a: %{
          b: "module.exports = require('./c/d').id;",
          c: %{
            d: "module.exports = require('./e');",
            e: "exports.id = module.id;"
          }
        }
      },
      # paths relative to module and parent
      idtest4: %{
        a: %{
          b: "module.exports = require('../a/./c/d').id;",
          c: %{
            d: "module.exports = require('./e');",
            e: "exports.id = module.id;"
          }
        }
      },
      # paths relative to root
      idtest5: %{
        a: "module.exports = require('whatever/idtest5/b').id;",
        b: "exports.id = module.id;"
      }
    },
    views: %{
      all_docs_twice: %{
        map: """
        function(doc) {
          emit(doc.integer, null);
          emit(doc.integer, null);
        }
        """
      },
      no_docs: %{
        map: """
        function(doc) {}
        """
      },
      single_doc: %{
        map: """
        function(doc) {
          if (doc._id === "1") {
            emit(1, null);
          }
        }
        """
      },
      summate: %{
        map: """
        function(doc) {
          emit(doc.integer, doc.integer);
        }
        """,
        reduce: """
        function(keys, values) {
          return sum(values);
        }
        """
      },
      summate2: %{
        map: """
        function(doc) {
          emit(doc.integer, doc.integer);
        }
        """,
        reduce: """
        function(keys, values) {
          return sum(values);
        }
        """
      },
      huge_src_and_results: %{
        map: """
         function(doc) {
           if (doc._id === "1") {
             emit("#{String.duplicate("a", 16)}", null);
           }
         }
        """,
        reduce: """
        function(keys, values) {
          return "#{String.duplicate("a", 16)}";
        }
        """
      },
      lib: %{
        baz: "exports.baz = 'bam';",
        foo: %{
          foo: "exports.foo = 'bar';",
          boom: "exports.boom = 'ok';",
          zoom: "exports.zoom = 'yeah';"
        }
      },
      commonjs: %{
        map: """
        function(doc) {
          emit(null, require('views/lib/foo/boom').boom);
        }
        """
      }
    },
    shows: %{
      simple: """
      function() {
        return 'ok';
      }
      """,
      requirey: """
      function() {
        var lib = require('whatever/commonjs/upper');
        return lib.testing;
      }
      """,
      circular: """
      function() {
        var lib = require('whatever/commonjs/upper');
        return JSON.stringify(this);
      }
      """,
      circular_require: """
      function() {
        return require('whatever/commonjs/circular_one').name;
      }
      """,
      idtest1: """
      function() {
        return require('whatever/idtest1/a/b/d');
      }
      """,
      idtest2: """
      function() {
        return require('whatever/idtest2/a/b/d');
      }
      """,
      idtest3: """
      function() {
        return require('whatever/idtest3/a/b');
      }
      """,
      idtest4: """
      function() {
        return require('whatever/idtest4/a/b');
      }
      """,
      idtest5: """
      function() {
        return require('whatever/idtest5/a');
      }
      """
    }
  }

  setup_all do
    db_name = random_db_name()
    {:ok, _} = create_db(db_name)
    on_exit(fn -> delete_db(db_name) end)

    {:ok, _} = create_doc(db_name, @design_doc)
    {:ok, _} = create_doc(db_name, %{})
    {:ok, [db_name: db_name]}
  end

  test "consistent _rev for design docs", context do
    resp = Couch.get("/#{context[:db_name]}/_design/test")
    assert resp.status_code == 200
    first_db_rev = resp.body["_rev"]

    second_db_name = random_db_name()
    create_db(second_db_name)
    {:ok, resp2} = create_doc(second_db_name, @design_doc)
    assert first_db_rev == resp2.body["rev"]
  end

  test "commonjs require", context do
    db_name = context[:db_name]
    resp = Couch.get("/#{db_name}/_design/test/_show/requirey")
    assert resp.status_code == 200
    assert resp.body == "PLANKTONwhatever/commonjs/upperplankton"

    resp = Couch.get("/#{db_name}/_design/test/_show/circular")
    assert resp.status_code == 200

    result =
      resp.body
      |> IO.iodata_to_binary()
      |> :jiffy.decode([:return_maps])

    assert result["language"] == "javascript"
  end

  test "circular commonjs dependencies", context do
    db_name = context[:db_name]
    resp = Couch.get("/#{db_name}/_design/test/_show/circular_require")
    assert resp.status_code == 200
    assert resp.body == "One"
  end

  test "module id values are as expected", context do
    db_name = context[:db_name]

    check_id_value(db_name, "idtest1", "whatever/idtest1/a/c/e")
    check_id_value(db_name, "idtest2", "whatever/idtest2/a/c/e")
    check_id_value(db_name, "idtest3", "whatever/idtest3/a/c/e")
    check_id_value(db_name, "idtest4", "whatever/idtest4/a/c/e")
    check_id_value(db_name, "idtest5", "whatever/idtest5/b")
  end

  defp check_id_value(db_name, id, expected) do
    resp = Couch.get("/#{db_name}/_design/test/_show/#{id}")
    assert resp.status_code == 200
    assert resp.body == expected
  end

  @tag :with_db
  test "that we get correct design doc info back", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @design_doc)

    resp = Couch.get("/#{db_name}/_design/test/_info")
    prev_view_sig = resp.body["view_index"]["signature"]
    prev_view_size = resp.body["view_index"]["sizes"]["file"]

    num_docs = 500
    bulk_save(db_name, make_docs(1..(num_docs + 1)))

    Couch.get("/#{db_name}/_design/test/_view/summate", query: [stale: "ok"])

    for _x <- 0..1 do
      resp = Couch.get("/#{db_name}/_design/test/_info")
      assert resp.body["name"] == "test"
      assert is_map(resp.body["view_index"])
      view_index = resp.body["view_index"]
      assert view_index["sizes"]["file"] == prev_view_size
      assert view_index["compact_running"] == false
      assert view_index["signature"] == prev_view_sig

      # check collator_versions result
      assert is_list(view_index["collator_versions"])
      collator_versions = view_index["collator_versions"]
      assert length(collator_versions) == 1
      version = hd(collator_versions)
      assert is_binary(version)
    end
  end

  test "commonjs in map functions", context do
    db_name = context[:db_name]

    resp = Couch.get("/#{db_name}/_design/test/_view/commonjs", query: [limit: 1])
    assert resp.status_code == 200
    assert Enum.at(resp.body["rows"], 0)["value"] == "ok"
  end

  test "_all_docs view returns correctly with keys", context do
    db_name = context[:db_name]

    resp =
      Couch.get("/#{db_name}/_all_docs",
        query: [startkey: :jiffy.encode("_design"), endkey: :jiffy.encode("_design0")]
      )

    assert length(resp.body["rows"]) == 1
  end

  @tag :with_db
  test "all_docs_twice", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @design_doc)

    num_docs = 500
    bulk_save(db_name, make_docs(1..(2 * num_docs)))

    for _x <- 0..1 do
      test_all_docs_twice(db_name, num_docs)
    end
  end

  defp test_all_docs_twice(db_name, num_docs) do
    resp = Couch.get("/#{db_name}/_design/test/_view/all_docs_twice")
    assert resp.status_code == 200
    rows = resp.body["rows"]

    for x <- 0..num_docs do
      assert Map.get(Enum.at(rows, 2 * x), "key") == x + 1
      assert Map.get(Enum.at(rows, 2 * x + 1), "key") == x + 1
    end

    resp = Couch.get("/#{db_name}/_design/test/_view/no_docs")
    assert resp.body["total_rows"] == 0

    resp = Couch.get("/#{db_name}/_design/test/_view/single_doc")
    assert resp.body["total_rows"] == 1
  end

  @tag :with_db
  test "language not specified, Javascript is implied", context do
    db_name = context[:db_name]
    bulk_save(db_name, make_docs(1..2))

    design_doc_2 = %{
      _id: "_design/test2",
      views: %{
        single_doc: %{
          map: """
          function(doc) {
            if (doc._id === "1") {
              emit(1, null);
            }
          }
          """
        }
      }
    }

    {:ok, _} = create_doc(db_name, design_doc_2)

    resp = Couch.get("/#{db_name}/_design/test2/_view/single_doc")
    assert resp.status_code == 200
    assert length(resp.body["rows"]) == 1
  end

  @tag :with_db
  test "startkey and endkey", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @design_doc)

    num_docs = 500
    bulk_save(db_name, make_docs(1..(2 * num_docs)))

    resp = Couch.get("/#{db_name}/_design/test/_view/summate")
    assert Enum.at(resp.body["rows"], 0)["value"] == summate(num_docs * 2)

    resp =
      Couch.get("/#{db_name}/_design/test/_view/summate",
        query: [startkey: 4, endkey: 4]
      )

    assert Enum.at(resp.body["rows"], 0)["value"] == 4

    resp =
      Couch.get("/#{db_name}/_design/test/_view/summate",
        query: [startkey: 4, endkey: 5]
      )

    assert Enum.at(resp.body["rows"], 0)["value"] == 9

    resp =
      Couch.get("/#{db_name}/_design/test/_view/summate",
        query: [startkey: 4, endkey: 6]
      )

    assert Enum.at(resp.body["rows"], 0)["value"] == 15

    # test start_key and end_key aliases
    resp =
      Couch.get("/#{db_name}/_design/test/_view/summate",
        query: [start_key: 4, end_key: 6]
      )

    assert Enum.at(resp.body["rows"], 0)["value"] == 15

    # Verify that a shared index (view def is an exact copy of "summate")
    # does not confuse the reduce stage
    resp =
      Couch.get("/#{db_name}/_design/test/_view/summate2",
        query: [startkey: 4, endkey: 6]
      )

    assert Enum.at(resp.body["rows"], 0)["value"] == 15

    for x <- 0..Integer.floor_div(num_docs, 60) do
      resp =
        Couch.get("/#{db_name}/_design/test/_view/summate",
          query: [startkey: x * 30, endkey: num_docs - x * 30]
        )

      assert Enum.at(resp.body["rows"], 0)["value"] ==
               summate(num_docs - x * 30) - summate(x * 30 - 1)
    end
  end

  defp summate(n) do
    (n + 1) * (n / 2)
  end

  @tag :with_db
  test "design doc deletion", context do
    db_name = context[:db_name]
    {:ok, resp} = create_doc(db_name, @design_doc)

    del_resp =
      Couch.delete("/#{db_name}/#{resp.body["id"]}", query: [rev: resp.body["rev"]])

    assert del_resp.status_code == 200

    resp = Couch.get("/#{db_name}/#{resp.body["id"]}")
    assert resp.status_code == 404

    resp = Couch.get("/#{db_name}/_design/test/_view/no_docs")
    assert resp.status_code == 404
  end

  @tag :with_db
  test "validate doc update", context do
    db_name = context[:db_name]

    # COUCHDB-1227 - if a design document is deleted, by adding a "_deleted"
    # field with the boolean value true, its validate_doc_update functions
    # should no longer have effect.

    ddoc = %{
      _id: "_design/test",
      language: "javascript",
      validate_doc_update: """
        function(newDoc, oldDoc, userCtx, secObj) {
         if (newDoc.value % 2 == 0) {
            throw({forbidden: "dont like even numbers"});
         }
         return true;
      }
      """
    }

    {:ok, resp_ddoc} = create_doc(db_name, ddoc)

    resp =
      Couch.post("/#{db_name}",
        body: %{_id: "doc1", value: 4}
      )

    assert resp.status_code == 403
    assert resp.body["reason"] == "dont like even numbers"

    ddoc_resp = Couch.get("/#{db_name}/#{resp_ddoc.body["id"]}")

    ddoc =
      ddoc_resp.body
      |> Map.put("_deleted", true)

    retry_until(fn ->
      retry_resp =
        Couch.post("/#{db_name}",
          body: ddoc
        )
      assert retry_resp.status_code in [201, 202]
      {:ok, _} = create_doc(db_name, %{_id: "doc1", value: 4})
    end)
  end
end
