defmodule ViewSandboxingTest do
  use CouchTestCase

  @document %{integer: 1, string: "1", array: [1, 2, 3]}

  @tag :with_db
  test "attempting to change the document has no effect", context do
    db_name = context[:db_name]

    {:ok, _} = create_doc(db_name, @document)

    map_fun = """
    function(doc) {
      doc.integer = 2;
      emit(null, doc);
    }
    """

    resp = query(db_name, map_fun, nil, %{include_docs: true})
    rows = resp["rows"]
    # either we have an error or our doc is unchanged
    assert resp["total_rows"] == 0 or Enum.at(rows, 0)["doc"]["integer"] == 1

    map_fun = """
    function(doc) {
      doc.array[0] = 0;
      emit(null, doc);
    }
    """

    resp = query(db_name, map_fun, nil, %{include_docs: true})
    row = Enum.at(resp["rows"], 0)
    # either we have an error or our doc is unchanged
    assert resp["total_rows"] == 0 or Enum.at(row["doc"]["array"], 0) == 1
  end

  @tag :with_db
  test "view cannot invoke interpreter internals", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @document)

    map_fun = """
    function(doc) {
      gc();
      emit(null, doc);
    }
    """

    # make sure that a view cannot invoke interpreter internals such as the
    # garbage collector
    resp = query(db_name, map_fun)
    assert resp["total_rows"] == 0
  end

  @tag :with_db
  test "view cannot access the map_funs and map_results array", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @document)

    map_fun = """
    function(doc) {
      map_funs.push(1);
      emit(null, doc);
    }
    """

    resp = query(db_name, map_fun)
    assert resp["total_rows"] == 0

    map_fun = """
    function(doc) {
      map_results.push(1);
      emit(null, doc);
    }
    """

    resp = query(db_name, map_fun)
    assert resp["total_rows"] == 0
  end

  @tag :with_db
  test "COUCHDB-925 - altering 'doc' variable in map function affects other map functions",
       context do
    db_name = context[:db_name]

    ddoc = %{
      _id: "_design/foobar",
      language: "javascript",
      views: %{
        view1: %{
          map: """
             function(doc) {
              if (doc.values) {
                doc.values = [666];
              }
              if (doc.tags) {
                doc.tags.push("qwerty");
              }
              if (doc.tokens) {
                doc.tokens["c"] = 3;
              }
            }
          """
        },
        view2: %{
          map: """
          function(doc) {
            if (doc.values) {
              emit(doc._id, doc.values);
            }
            if (doc.tags) {
              emit(doc._id, doc.tags);
            }
            if (doc.tokens) {
              emit(doc._id, doc.tokens);
            }
          }
          """
        }
      }
    }

    doc1 = %{
      _id: "doc1",
      values: [1, 2, 3]
    }

    doc2 = %{
      _id: "doc2",
      tags: ["foo", "bar"],
      tokens: %{a: 1, b: 2}
    }

    {:ok, _} = create_doc(db_name, ddoc)
    {:ok, _} = create_doc(db_name, doc1)
    {:ok, _} = create_doc(db_name, doc2)

    resp1 = view(db_name, "foobar/view1")
    resp2 = view(db_name, "foobar/view2")

    assert Enum.empty?(resp1.body["rows"])
    assert length(resp2.body["rows"]) == 3

    assert doc1[:_id] == Enum.at(resp2.body["rows"], 0)["key"]
    assert doc2[:_id] == Enum.at(resp2.body["rows"], 1)["key"]
    assert doc2[:_id] == Enum.at(resp2.body["rows"], 2)["key"]

    assert length(Enum.at(resp2.body["rows"], 0)["value"]) == 3

    row0_values = Enum.at(resp2.body["rows"], 0)["value"]

    assert Enum.at(row0_values, 0) == 1
    assert Enum.at(row0_values, 1) == 2
    assert Enum.at(row0_values, 2) == 3

    row1_values = Enum.at(resp2.body["rows"], 1)["value"]
    row2_values = Enum.at(resp2.body["rows"], 2)["value"]

    # we can't be 100% sure about the order for the same key
    assert (is_map(row1_values) and row1_values["a"] == 1) or
             (is_list(row1_values) and Enum.at(row1_values, 0) == "foo")

    assert (is_map(row1_values) and row1_values["b"] == 2) or
             (is_list(row1_values) and Enum.at(row1_values, 1) == "bar")

    assert (is_map(row2_values) and row2_values["a"] == 1) or
             (is_list(row2_values) and Enum.at(row2_values, 0) == "foo")

    assert (is_map(row2_values) and row2_values["b"] == 2) or
             (is_list(row2_values) and Enum.at(row2_values, 1) == "bar")

    assert is_list(row1_values) or !Map.has_key?(row1_values, "c")
    assert is_list(row2_values) or !Map.has_key?(row2_values, "c")
  end

  @tag :with_db
  test "runtime code evaluation can be prevented", context do
    db_name = context[:db_name]
    {:ok, _} = create_doc(db_name, @document)

    map_fun = """
    function(doc) {
      var glob = emit.constructor('return this')();
      emit(doc._id, null);
    }
    """

    resp = query(db_name, map_fun)

    case quickjs() do
      true ->
        # QuickJS uses runtime instances for isolation
        assert resp["total_rows"] == 1
      false ->
        assert resp["total_rows"] == 0
    end
  end

  defp quickjs() do
    versions = Couch.get("/_node/_local/_versions")
    assert versions.status_code == 200
    case versions.body["javascript_engine"]["name"] do
      "quickjs" ->
        true
      _ ->
        false
    end
  end
end
