# Copyright (C) 2009-2013 MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'test_helper'
require 'json'

module Mongo
  class Collection
    public :batch_write
  end

  class BulkWriteCollectionView
    public :update_doc?, :replace_doc?, :nil_tally, :merge_result

    # for reference and future server direction
    def generate_batch_commands(groups, write_concern)
      groups.collect do |op_type, documents|
        {
            op_type => @collection.name,
            Mongo::CollectionWriter::WRITE_COMMAND_ARG_KEY[op_type] => documents,
            :ordered => @options[:ordered],
            :writeConcern => write_concern
        }
      end
    end
  end

  class MongoDBError
    def inspect
      "#{self.class.name}.new(#{message.inspect},#{error_code.inspect},#{result.inspect})"
    end
  end
end

module BSON
  class InvalidDocument
    def inspect
      "#{self.class.name}.new(#{message.inspect})"
    end
  end
end

class BulkWriteCollectionViewTest < Test::Unit::TestCase
  @@client ||= standard_connection(:op_timeout => 10)
  @@db = @@client.db(TEST_DB)
  @@test = @@db.collection("test")
  @@version = @@client.server_version

  DATABASE_NAME = 'ruby_test_bulk_write_collection_view'
  COLLECTION_NAME = 'test'
  DUPLICATE_KEY_ERROR_CODE_SET = [11000, 11001, 12582, 16460].to_set

  def assert_bulk_op_pushed(expected, view)
    assert_equal expected, view.ops.last
  end

  def assert_is_bulk_write_collection_view(view)
    assert_equal Mongo::BulkWriteCollectionView, view.class
  end

  def assert_bulk_exception(expected, message = '')
    ex = assert_raise BulkWriteError, message do
      pp yield
    end
    assert_equal(Mongo::ErrorCode::MULTIPLE_ERRORS_OCCURRED, ex.error_code, message)
    assert_match_document(expected, ex.result, message)
  end

  def default_setup
    @client = MongoClient.new
    @db = @client[DATABASE_NAME]
    @collection = @db[COLLECTION_NAME]
    @collection.drop
    @bulk = @collection.initialize_ordered_bulk_op
    @q = {:a => 1}
    @u = {"$inc" => {:x => 1}}
    @r = {:b => 2}
  end

  def sort_docs(docs)
    docs.sort{|a,b| [a.keys, a.values] <=> [b.keys, b.values]}
  end

  def generate_sized_doc(size)
    doc = {"_id" => BSON::ObjectId.new, "x" => "y"}
    serialize_doc = BSON::BSON_CODER.serialize(doc, false, false, size)
    doc = {"_id" => BSON::ObjectId.new, "x" => "y" * (1 + size - serialize_doc.size)}
    assert_equal size, BSON::BSON_CODER.serialize(doc, false, false, size).size
    doc
  end

  context "Bulk API Collection" do
    setup do
      default_setup
    end

    should "inspect" do
      assert_equal String, @bulk.inspect.class
    end

    should "check first key is operation for #update_doc?" do
      assert_not_nil @bulk.update_doc?({"$inc" => {:x => 1}})
      assert_false @bulk.update_doc?({})
      assert_nil @bulk.update_doc?({:x => 1})
    end

    should "check no top-level key is operation for #replace_doc?" do
      assert_true @bulk.replace_doc?({:x => 1})
      assert_true @bulk.replace_doc?({})
      assert_false @bulk.replace_doc?({"$inc" => {:x => 1}})
      assert_false @bulk.replace_doc?({:a => 1, "$inc" => {:x => 1}})
    end

    should "generate_batch_commands" do
      groups = [
          [:insert, [{:n => 0}]],
          [:update, [{:n => 1}, {:n => 2}]],
          [:delete, [{:n => 3}]],
          [:insert, [{:n => 5}, {:n => 6}, {:n => 7}]],
          [:update, [{:n => 8}]],
          [:delete, [{:n => 9}, {:n => 10}]]
      ]
      write_concern = {:w => 1}
      result = @bulk.generate_batch_commands(groups, write_concern)
      expected = [
          {:insert => COLLECTION_NAME, :documents => [{:n => 0}], :ordered => true, :writeConcern => {:w => 1}},
          {:update => COLLECTION_NAME, :updates => [{:n => 1}, {:n => 2}], :ordered => true, :writeConcern => {:w => 1}},
          {:delete => COLLECTION_NAME, :deletes => [{:n => 3}], :ordered => true, :writeConcern => {:w => 1}},
          {:insert => COLLECTION_NAME, :documents => [{:n => 5}, {:n => 6}, {:n => 7}], :ordered => true, :writeConcern => {:w => 1}},
          {:update => COLLECTION_NAME, :updates => [{:n => 8}], :ordered => true, :writeConcern => {:w => 1}},
          {:delete => COLLECTION_NAME, :deletes => [{:n => 9}, {:n => 10}], :ordered => true, :writeConcern => {:w => 1}}
      ]
      assert_equal expected, result
    end

    should "Initialize an unordered bulk op - spec Bulk Operation Builder" do
      @bulk = @collection.initialize_unordered_bulk_op
      assert_is_bulk_write_collection_view(@bulk)
      assert_equal @collection, @bulk.collection
      assert_equal false, @bulk.options[:ordered]
    end

    should "Initialize an ordered bulk op - spec Bulk Operation Builder" do
      assert_is_bulk_write_collection_view(@bulk)
      assert_equal @collection, @bulk.collection
      assert_equal true, @bulk.options[:ordered]
    end
  end

  def big_example(bulk)
    bulk.insert({:a => 1})
    bulk.insert({:a => 2})
    bulk.insert({:a => 3})
    bulk.insert({:a => 4})
    bulk.insert({:a => 5})
    # Update one document matching the selector
    bulk.find({:a => 1}).update_one({"$inc" => {:x => 1}})
    # Update all documents matching the selector
    bulk.find({:a => 2}).update({"$inc" => {:x => 2}})
    # Replace entire document (update with whole doc replace)
    bulk.find({:a => 3}).replace_one({:x => 3})
    # Update one document matching the selector or upsert
    bulk.find({:a => 1}).upsert.update_one({"$inc" => {:x => 1}})
    # Update all documents matching the selector or upsert
    bulk.find({:a => 2}).upsert.update({"$inc" => {:x => 2}})
    # Replaces a single document matching the selector or upsert
    bulk.find({:a => 3}).upsert.replace_one({:x => 3})
    # Remove a single document matching the selector
    bulk.find({:a => 4}).remove_one()
    # Remove all documents matching the selector
    bulk.find({:a => 5}).remove()
    # Insert a document
    bulk.insert({:x => 4})
  end

  def nil_tally_responses(responses, key)
    result = {}
    responses.each do |response|
      @bulk.nil_tally(result, key, response[key])
    end
    result
  end

  context "Bulk API CollectionView" do
    setup do
      default_setup
    end

    # ----- INSERT -----

    should "set :insert, :documents, terminate and return view for #insert" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        document = {:a => 5}
        result = @bulk.insert(document)
        assert_is_bulk_write_collection_view(result)
        assert_bulk_op_pushed [:insert, {:d => document}], @bulk
        result = @bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nInserted" => 1
            }, result, "wire_version:#{wire_version}")
        assert_equal 1, @collection.count
      end
    end

    should "error out on $-prefixed keys with #insert" do
      assert_raise BulkWriteError do
        @bulk.insert({ "$key" => 1 })
        @bulk.execute
      end
    end

    should "attempt to run #insert with find() and succeed, ignoring find()" do
      @bulk.find({}).insert({})
      @bulk.execute
    end

    # ----- FIND -----

    should "set :q and return view for #find" do
      result = @bulk.find(@q)
      assert_is_bulk_write_collection_view(result)
      assert_equal @q, @bulk.op_args[:q]
      @bulk.find({})
      assert_equal({}, @bulk.op_args[:q])
      @bulk.find(:a => 1)
      assert_equal({:a => 1}, @bulk.op_args[:q])
    end

    should "raise an exception for empty #find" do
      assert_raise MongoArgumentError do
        @bulk.find({})
        @bulk.execute
      end
    end

    # ----- UPDATE -----

    should "set :upsert for #upsert" do
      result = @bulk.find(@q).upsert
      assert_is_bulk_write_collection_view(result)
      assert_true result.op_args[:upsert]
    end

    should "check arg for update, set :update, :u, :multi, terminate and return view for #update" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1, :b => 1})
        @collection.insert({:a => 2, :b => 1})
        @collection.insert({:a => 2, :b => 1})
        bulk = @collection.initialize_ordered_bulk_op

        u = {"$inc" => {:b => 1}}
        q = {:a => 2}

        assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
          bulk.update(u)
        end
        assert_raise_error(MongoArgumentError, "document must start with an operator") do
          bulk.find(q).update(q)
        end

        result = bulk.find({:a => 2}).update(u)
        assert_is_bulk_write_collection_view(result)
        assert_bulk_op_pushed [:update, {:q => q, :u => u, :multi => true}], bulk

        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 2,
                "nMatched" => 2,
                "nModified" => batch_commands?(wire_version) ? 2 : nil,
            }, result, "wire_version:#{wire_version}")
        assert_equal 1, @collection.find({:b => 1}).count
      end
    end

    # ----- UPDATE_ONE -----

    should "check arg for update, set :update, :u, :multi, terminate and return view for #update_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1})
        @collection.insert({:a => 1, :b => 2})
        bulk = @collection.initialize_ordered_bulk_op

        assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
          bulk.update_one(@u)
        end
        assert_raise_error(MongoArgumentError, "document must start with an operator") do
          bulk.find(@q).update_one(@r)
        end
        result = bulk.find(@q).update_one(@u)
        assert_is_bulk_write_collection_view(result)
        assert_bulk_op_pushed [:update, {:q => @q, :u => @u, :multi => false}], bulk
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 1,
                "nModified" => batch_commands?(wire_version) ? 1 : nil,
            }, result, "wire_version:#{wire_version}")
        assert_equal 2, @collection.count
      end
    end

    should "error-out in server when $-prefixed key is passed to #update_one" do
      assert_raise BulkWriteError do
        oh = BSON::OrderedHash.new
        oh["$key"] = 1
        oh[:a] = 1
        @bulk.find(@q).update(oh)
        @bulk.execute
      end
    end

    should "error-out in driver when first field passed to #update_one is not operator" do
      assert_raise_error(MongoArgumentError, "document must start with an operator") do
        oh = BSON::OrderedHash.new
        oh[:a] = 1
        oh["$key"] = 1
        @bulk.find(@q).update(oh)
      end
    end

    # ----- REPLACE_ONE -----

    should "check arg for replacement, set :update, :u, :multi, terminate and return view for #replace_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1})
        @collection.insert({:a => 1})
        bulk = @collection.initialize_ordered_bulk_op
        q = {:a => 1}
        r = {:a => 2}

        assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
          bulk.replace_one(q)
        end
        assert_raise_error(MongoArgumentError, "document must not contain any operators") do
          bulk.find(q).replace_one(@u)
        end

        result = bulk.find(q).replace_one(r)
        assert_is_bulk_write_collection_view(result)
        assert_bulk_op_pushed [:update, {:q => q, :u => r, :multi => false}], bulk

        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 1,
                "nModified" => batch_commands?(wire_version) ? 1 : nil,
            }, result, "wire_version:#{wire_version}")
        assert_equal 1, @collection.find(q).count
      end
    end

    # ----- REMOVE -----

    should "remove all documents when empty selector is passed to #remove" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.insert({:a => 1})
        @collection.insert({:a => 2})
        @bulk.find({}).remove
        result = @bulk.execute
        assert_equal 0, @collection.count
      end
    end

    should "#remove only documents that match selector" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1})
        @collection.insert({:a => 2})
        @bulk.find({:a => 1}).remove
        result = @bulk.execute
        assert_equal 1, @collection.count
        # should fail if we re-execute
        assert_raise_error(MongoArgumentError, "batch is empty") do
          @bulk.execute
        end
      end
    end

    should "set :remove, :q, :limit, terminate and return view for #remove" do
      assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
        @bulk.remove
      end
      result = @bulk.find(@q).remove
      assert_is_bulk_write_collection_view(result)
      assert_bulk_op_pushed [:delete, {:q => @q, :limit => 0}], @bulk
    end

    # ----- REMOVE_ONE -----

    should "set :remove, :q, :limit, terminate and return view for #remove_one" do
      assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
        @bulk.remove_one
      end
      result = @bulk.find(@q).remove_one
      assert_is_bulk_write_collection_view(result)
      assert_bulk_op_pushed [:delete, {:q => @q, :limit => 1}], @bulk
    end

    should "remove only one of several matching documents for #remove_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1, :b => 1})
        @collection.insert({:a => 1, :b => 2})
        @bulk.find({:a => 1}).remove_one
        result = @bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nRemoved" => 1,
                "nModified" => nil,
            }, result, "wire_version:#{wire_version}")
        assert_equal 1, @collection.count
      end
    end

    # ----- UPSERT-UPDATE -----

    should "handle single upsert - spec Handling upserts" do # chose array always for upserted value
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_ordered_bulk_op

        assert_raise_error(MongoArgumentError, "non-nil query must be set via find") do
          @bulk.upsert.update({"$set" => {:a => 1}})
        end

        bulk.find({:a => 1}).upsert.update({'$set' => {:a => 2}})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 0,
                "nUpserted" => 1,
                "nModified" => batch_commands?(wire_version) ? 0 : nil,
                "upserted" => [
                    {"_id" => BSON::ObjectId('52a16767bb67fbc77e26a310'), "index" => 0}
                ]
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "run #upsert.update without affecting non-upsert updates" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove

        bulk = @collection.initialize_unordered_bulk_op
        bulk.find({:a => 1}).update({"$set" => {:x => 1}})
        bulk.find({:a => 2}).upsert.update({"$set" => {:x => 2}})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 0,
                "nModified" => batch_commands?(wire_version) ? 0 : nil,
                "nUpserted" => 1,
                "upserted" => [
                    {"_id" => BSON::ObjectId('52a16767bb67fbc77e26a310'), "index" => 1}
                ]
            }, result, "wire_version:#{wire_version}")

        # Repeat the batch and nMatched = 1, nUpserted = 0
        bulk2 = @collection.initialize_unordered_bulk_op
        bulk2.find({:a => 1}).update({"$set" => {:x => 1}})
        bulk2.find({:a => 2}).upsert.update({"$set" => {:x => 2}})
        result2 = bulk2.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 1,
                "nModified" => batch_commands?(wire_version) ? 0 : nil
            }, result2, "wire_version:#{wire_version}")
      end
    end

    # ----- UPSERT-UPDATE_ONE -----

    should "#upsert a document without affecting non-upsert update_ones" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        bulk = @collection.initialize_unordered_bulk_op

        bulk.find({:a => 1}).update_one({"$set" => {:x => 1}})
        bulk.find({:a => 2}).upsert.update_one({"$set" => {:x => 2}})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 0,
                "nUpserted" => 1,
                "nModified" => batch_commands?(wire_version) ? 0 : nil,
                "upserted" => [
                    {"_id" => BSON::ObjectId('52a16767bb67fbc77e26a310'), "index" => 1}
                ]
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "only update one matching document with #upsert-update_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1})
        @collection.insert({:a => 1})

        bulk = @collection.initialize_unordered_bulk_op
        bulk.find({:a => 1}).update_one({"$set" => {:x => 1}})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 1,
                "nModified" => batch_commands?(wire_version) ? 1 : nil,
            }, result, "wire_version:#{wire_version}")
      end
    end

    # ----- UPSERT-REPLACE_ONE -----

    should "not affect non-upsert replace_ones in same batch as #upsert-replace_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        bulk = @collection.initialize_unordered_bulk_op
        bulk.find({:a => 1}).replace_one({:x => 1})
        bulk.find({:a => 2}).upsert.replace_one({:x => 2})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "nMatched" => 0,
                "nUpserted" => 1,
                "nModified" => batch_commands?(wire_version) ? 0 : nil,
                "upserted" => [
                    {"_id" => BSON::ObjectId('52a16767bb67fbc77e26a310'), "index" => 1}
                ]
            }, result, "wire_version:#{wire_version}")
        assert_equal 1, @collection.count
      end
    end

    should "only replace one matching document with #upsert-replace_one" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.insert({:a => 1})
        @collection.insert({:a => 1})
        bulk = @collection.initialize_unordered_bulk_op
        bulk.find({:a => 1}).replace_one({:x => 1})
        bulk.find({:a => 2}).upsert.replace_one({:x => 2})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 2,
                "nMatched" => 1,
                "nUpserted" => 1,
                "nModified" => batch_commands?(wire_version) ? 1 : nil,
                "upserted" => [
                    {"_id" => BSON::ObjectId('52a16767bb67fbc77e26a310'), "index" => 1}
                ]
            }, result, "wire_version:#{wire_version}")
        assert_equal 3, @collection.count
      end
    end

    should "tally given all numbers or return nil for #nil_tally" do
      assert_equal({"nM" => 6}, nil_tally_responses([{"nM" => 1}, {"nM" => 2}, {"nM" => 3}], "nM"))
      assert_equal({"nM" => nil}, nil_tally_responses([{"nM" => 1}, { }, {"nM" => 3}], "nM"))
      assert_equal({"nM" => nil}, nil_tally_responses([{"nM" => 1}, {"nM" => nil}, {"nM" => 3}], "nM"))
    end

    # ----- MIXED OPS, ORDERED -----

    should "execute, return result and reset @ops for #execute" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk.insert({:x => 1})
        @bulk.insert({:x => 2})
        write_concern = {:w => 1}
        result = @bulk.execute(write_concern)
        assert_equal({"ok" => 1, "n" => 2, "nInserted" => 2}, result, "wire_version:#{wire_version}")
        assert_equal 2, @collection.count
        assert_equal [], @bulk.ops
      end
    end

    should "run ordered big example" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        big_example(@bulk)
        write_concern = {:w => 1} #{:w => 1. :j => 1} #nojournal for tests
        result = @bulk.execute(write_concern)
        assert_match_document(
            {
                "ok" => 1,
                "n" => 14,
                "nInserted" => 6,
                "nMatched" => 5,
                "nUpserted" => 1,
                "nModified" => batch_commands?(wire_version) ? 5 : nil,
                "nRemoved" => 2,
                "upserted" => [
                    {
                        "index" => 10,
                        "_id" => BSON::ObjectId('52a1e4a4bb67fbc77e26a340')
                    }
                ]
            }, result, "wire_version:#{wire_version}")
        assert_equal(batch_commands?(wire_version), result.has_key?("nModified"), "wire_version:#{wire_version}")
        assert_false(@collection.find.to_a.empty?, "wire_version:#{wire_version}")
        assert_equal [{"a"=>1, "x"=>2}, {"a"=>2, "x"=>4}, {"x"=>3}, {"x"=>3}, {"x"=>4}], sort_docs(@collection.find.to_a.collect { |doc| doc.delete("_id"); doc })
      end
    end

    should "run spec Ordered Bulk Operations" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @bulk.insert({:a => 1})
        @bulk.insert({:a => 2})
        @bulk.insert({:a => 3})
        @bulk.find({:a => 2}).upsert.update({'$set' => {:a => 4}})
        @bulk.find({:a => 1}).remove_one
        @bulk.insert({:a => 5})
        result = @bulk.execute({:w => 1})
        assert_match_document(
            {
                "ok" => 1,
                "n" => 6,
                "nInserted" => 4,
                "nMatched" => 1,
                "nModified" => batch_commands?(wire_version) ? 1 : nil,
                "nRemoved" => 1,
            }, result, "wire_version:#{wire_version}")
      end
    end

    # ----- MIXED OPS, UNORDERED -----

    should "run unordered big example" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk = @collection.initialize_unordered_bulk_op
        big_example(@bulk)
        write_concern = {:w => 1} #{:w => 1. :j => 1} #nojournal for tests
        result = @bulk.execute(write_concern)
        assert_equal(6, result["nInserted"])
        assert_true(result["n"] > 0, "wire_version:#{wire_version}")
        assert_equal(batch_commands?(wire_version), result.has_key?("nModified"), "wire_version:#{wire_version}")
        assert_false(@collection.find.to_a.empty?, "wire_version:#{wire_version}")
      end
    end

    should "run unordered big example with w 0" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk = @collection.initialize_unordered_bulk_op
        big_example(@bulk)
        write_concern = {:w => 0}
        result = @bulk.execute(write_concern)
        assert_equal(true, result, "wire_version:#{wire_version}")
        assert_false(@collection.find.to_a.empty?, "wire_version:#{wire_version}")
      end
    end

    should "run unordered bulk operations in one batch per write-type" do
      with_write_commands(@db.connection) do
        @collection.expects(:batch_write).at_most(3).returns([[], [], [], []])
        bulk = @collection.initialize_unordered_bulk_op
        bulk.insert({:_id => 1, :a => 1})
        bulk.find({:_id => 1, :a => 1}).update({"$inc" => {:x => 1}})
        bulk.find({:_id => 1, :a => 1}).remove
        bulk.insert({:_id => 2, :a => 2})
        bulk.find({:_id => 2, :a => 2}).update({"$inc" => {:x => 2}})
        bulk.find({:_id => 2, :a => 2}).remove
        bulk.insert({:_id => 3, :a => 3})
        bulk.find({:_id => 3, :a => 3}).update({"$inc" => {:x => 3}})
        bulk.find({:_id => 3, :a => 3}).remove
        result = bulk.execute # unordered varies, don't use assert_match_document
      end
    end

    should "run spec Unordered Bulk Operations" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        bulk = @collection.initialize_unordered_bulk_op
        bulk.insert({:_id => 1})
        bulk.find({:_id => 2}).update_one({'$inc' => { :x => 1 }})
        bulk.find({:_id => 3}).remove_one
        bulk.insert({:_id => 4})
        bulk.find({:_id => 5}).update_one({'$inc' => { :x => 1 }})
        bulk.find({:_id => 6}).remove_one
        result = nil
        begin
          result = bulk.execute
        rescue => ex
          result = ex.result
        end
        # for write commands internally the driver will execute 3. One each for the inserts, updates and removes.
      end
    end

    # ----- EMPTY BATCH -----

    should "handle empty bulk op" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        assert_raise_error(MongoArgumentError, Mongo::BulkWriteCollectionView::EMPTY_BATCH_MSG) do
          @bulk.execute
        end
      end
    end

    should "handle insert of overly large document" do
      large_doc = {"a" => "y"*(2*@client.max_message_size)}
      with_write_commands_and_operations(@db.connection) do |wire_version|
        ex = assert_raise Mongo::BulkWriteError do
          @collection.remove
          bulk = @collection.initialize_unordered_bulk_op
          bulk.insert(large_doc)
          puts "bulk.execute:#{bulk.execute.inspect}"
        end
        assert_equal 22, ex.result["writeErrors"].first["code"]
      end
    end

    # ----- ORDERED, WITH ERRORS -----

    should "handle error for duplicate key with offset" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk.find({:a => 1}).update_one({"$inc" => {:x => 1}})
        @bulk.insert({:_id => 1, :a => 1})
        @bulk.insert({:_id => 1, :a => 2})
        @bulk.insert({:_id => 3, :a => 3})
        ex = assert_raise BulkWriteError do
          @bulk.execute
        end
        result = ex.result
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "writeErrors" =>
                    [{
                      "index" => 2,
                      "code" => 11000,
                      "errmsg" => /duplicate key error/
                    }],
                "code" => 65,
                "errmsg" => "batch item errors occurred",
                "nInserted" => 1,
                "nMatched" => 0,
                "nModified" => batch_commands?(wire_version) ? 0 : nil
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "handle error for serialization with offset" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        assert_equal 16777216, @@client.max_bson_size
        @bulk.find({:a => 1}).update_one({"$inc" => {:x => 1}})
        @bulk.insert({:_id => 1, :a => 1})
        @bulk.insert(generate_sized_doc(@@client.max_message_size + 1))
        @bulk.insert({:_id => 3, :a => 3})
        ex = assert_raise BulkWriteError do
          @bulk.execute
        end
        result = ex.result
        assert_match_document(
            {
                "ok" => 1,
                "n" => 1,
                "writeErrors" =>
                    [{
                         "index" => 2,
                         "code" => 22,
                         "errmsg" => /too large/
                     }],
                "code" => 65,
                "errmsg" => "batch item errors occurred",
                "nInserted" => 1,
                "nMatched" => 0,
                "nModified" => batch_commands?(wire_version) ? 0 : nil
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "run ordered bulk op - spec Modes of Execution" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        @bulk.insert({:a => 1})
        @bulk.insert({:a => 2})
        @bulk.find({:a => 2}).update({'$set' => {:a => 1}}) # Clashes with unique index
        @bulk.find({:a => 1}).remove
        ex = assert_raise BulkWriteError do
          @bulk.execute
        end
        assert_equal(2, @collection.count)
      end
    end

    should "handle duplicate key error - spec Merging Results" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_ordered_bulk_op
        bulk.insert({:a => 1})
        bulk.insert({:a => 2})
        bulk.find({:a => 2}).upsert.update({'$set' => {:a => 1}})
        bulk.insert({:a => 3})
        ex = assert_raise BulkWriteError do
          bulk.execute
        end
        result = ex.result
        assert_match_document(
            {
                "ok" => 1,
                "n" => 2,
                "writeErrors" =>
                    [{
                         "index" => 2,
                         "code" => DUPLICATE_KEY_ERROR_CODE_SET,
                         "errmsg" => /duplicate key error/
                     }],
                "code" => 65,
                "errmsg" => "batch item errors occurred",
                "nInserted" => 2,
                "nMatched" => 0,
                "nModified" => batch_commands?(wire_version) ? 0 : nil
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "report user index - spec Merging errors" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_ordered_bulk_op
        bulk.insert({:a => 1})
        bulk.insert({:a => 2})
        bulk.find({:a => 2}).update_one({'$set' => {:a => 1}});
        bulk.find({:a => 4}).remove_one();
        ex = assert_raise BulkWriteError do
          bulk.execute({:w => 1})
        end
        result = ex.result
        assert_match_document(
            {
                "ok" => 1,
                "n" => 2,
                "writeErrors" =>
                    [{
                         "index" => 2,
                         "code" => DUPLICATE_KEY_ERROR_CODE_SET,
                         "errmsg" => /duplicate key error/
                     }],
                "code" => 65,
                "errmsg" => "batch item errors occurred",
                "nInserted" => 2,
                "nMatched" => 0,
                "nModified" => batch_commands?(wire_version) ? 0 : nil
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "handle multiple upsert - spec Handling upserts" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_ordered_bulk_op
        bulk.find({:a => 1}).upsert.update({'$set' => {:a => 2}})
        bulk.find({:a => 3}).upsert.update({'$set' => {:a => 4}})
        result = bulk.execute
        assert_match_document(
            {
                "ok" => 1,
                "n" => 2,
                "nMatched" => 0,
                "nUpserted" => 2,
                "nModified" => batch_commands?(wire_version) ? 0 : nil,
                "upserted" => [
                    {"index" => 0, "_id" => BSON::ObjectId('52a1e37cbb67fbc77e26a338')},
                    {"index" => 1, "_id" => BSON::ObjectId('52a1e37cbb67fbc77e26a339')}
                ]
            }, result, "wire_version:#{wire_version}")
      end
    end

    should "handle replication usage error" do
      with_no_replication(@db.connection) do
        with_write_commands_and_operations(@db.connection) do |wire_version|
          @collection.remove
          @bulk = @collection.initialize_ordered_bulk_op
          @bulk.insert({:_id => 1, :a => 1})
          write_concern = {:w => 5}
          ex = assert_raise BulkWriteError do
            @bulk.execute(write_concern)
          end
          result = ex.result
          if @@version >= "2.5.5"
            assert_match_document(
                {
                    "ok" => 0,
                    "n" => 0,
                    "code" => 65,
                    "errmsg" => "batch item errors occurred",
                    "writeErrors" => [
                        {
                            "errmsg" => "cannot use 'w' > 1 when a host is not replicated",
                            "code" => 2,
                            "index" => 0}
                    ],
                    "nInserted" => 0,
                }, result, "wire_version:#{wire_version}")
          else
            assert_match_document(
                {
                    "ok" => 1,
                    "n" => 1,
                    "code" => 65,
                    "errmsg" => "batch item errors occurred",
                    "writeConcernError" => [
                        {
                            "errmsg" => /no replication has been enabled/,
                            "code" => 64,
                            "index" => 0
                        }
                    ],
                    "nInserted" => 1,
                }, result, "wire_version:#{wire_version}")
          end
        end
      end
    end

    # ----- UNORDERED, WITH ERRORS -----

    should "handle error for unordered multiple duplicate key with offset" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk = @collection.initialize_unordered_bulk_op
        @bulk.find({:a => 1}).remove
        @bulk.insert({:_id => 1, :a => 1})
        @bulk.insert({:_id => 1, :a => 2})
        @bulk.insert({:_id => 3, :a => 3})
        @bulk.insert({:_id => 3, :a => 3})
        ex = assert_raise BulkWriteError do
          @bulk.execute
        end
        result = ex.result
        assert_true (0 < result["nInserted"] && result["nInserted"] < 3)
        assert_not_nil(result["writeErrors"], "wire_version:#{wire_version}")
      end
    end

    should "run unordered bulk op - spec Modes of Execution" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_unordered_bulk_op
        bulk.insert({:a => 1})
        bulk.insert({:a => 2})
        bulk.find({:a => 2}).update({'$set' => {:a => 1}}) # Clashes with unique index
        bulk.find({:a => 3}).remove
        bulk.find({:a => 2}).update({'$set' => {:a => 1}}) # Clashes with unique index
        ex = assert_raise BulkWriteError do
          bulk.execute
        end
        result = ex.result
        assert(result["writeErrors"].size > 1, "wire_version:#{wire_version}")
      end
    end

    should "handle unordered errors - spec Merging Results" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @collection.ensure_index(BSON::OrderedHash[:a, Mongo::ASCENDING], {:unique => true})
        bulk = @collection.initialize_unordered_bulk_op
        bulk.insert({:a => 1})
        bulk.find({:a => 1}).upsert.update({'$set' => {:a => 2}})
        bulk.insert({:a => 2})
        ex = assert_raise BulkWriteError do
          bulk.execute
        end
        result = ex.result # unordered varies, don't use assert_bulk_exception
        assert_equal(1, result['ok'], "wire_version:#{wire_version}")
        assert_equal(2, result['n'], "wire_version:#{wire_version}")
        err_details = result['writeErrors']
        assert_match(/duplicate key error/, err_details.first['errmsg'], "wire_version:#{wire_version}")
      end
    end

    should "handle multiple errors for unordered bulk write" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk = @collection.initialize_unordered_bulk_op
        @bulk.insert({:_id => 1, :a => 1})
        @bulk.insert({:_id => 1, :a => 2})
        @bulk.insert(generate_sized_doc(@@client.max_message_size + 1))
        @bulk.insert({:_id => 3, :a => 3})
        @bulk.find({:a => 4}).upsert.replace_one({:x => 3})
        ex = assert_raise BulkWriteError do
          @bulk.execute
        end
        result = ex.result # unordered varies, don't use assert_bulk_exception
        assert_equal(1, result['ok'], "wire_version:#{wire_version}")
        assert_equal(3, result['n'], "wire_version:#{wire_version}")
        err_details = result['writeErrors']
        assert_match(/duplicate key error/, err_details.find { |e| e['code']==11000 }['errmsg'], "wire_version:#{wire_version}")
        assert_match(/too large/, err_details.find { |e| e['index']==2 }['errmsg'], "wire_version:#{wire_version}")
        assert_not_nil(result['upserted'].find { |e| e['index']==4 }, "wire_version:#{wire_version}")
      end
    end

    # ----- NO_JOURNAL -----

    should "handle journaling error" do
      with_no_journaling(@db.connection) do
        with_write_commands_and_operations(@db.connection) do |wire_version|
          @collection.remove
          @bulk = @collection.initialize_ordered_bulk_op
          @bulk.insert({:_id => 1, :a => 1})
          write_concern = {:w => 1, :j => 1}
          ex = assert_raise BulkWriteError do
            @bulk.execute(write_concern)
          end
          result = ex.result
          if @@version >= "2.5.5"
            assert_match_document(
                {
                    "ok" => 0,
                    "n" => 0,
                    "writeErrors" => [
                        {
                            "code" => 2,
                            "errmsg" => "cannot use 'j' option when a host does not have journaling enabled", "index" => 0
                        }
                    ],
                    "code" => 65,
                    "errmsg" => "batch item errors occurred",
                    "nInserted" => 0
                }, result, "wire_version:#{wire_version}")
          else
            assert_match_document(
                {
                    "ok" => 1,
                    "n" => 1,
                    "writeConcernError" => [
                        {
                            "code" => 2,
                            "errmsg" => "journaling not enabled on this server",
                            "index" => 0
                        }
                    ],
                    "code" => 65,
                    "errmsg" => "batch item errors occurred",
                    "nInserted" => 1
                }, result, "wire_version:#{wire_version}")
          end
        end
      end
    end

    # ----- W = 0 -----

    should "run ordered big example with w 0" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        big_example(@bulk)
        result = @bulk.execute({:w => 0})
        assert_equal(true, result, "wire_version:#{wire_version}")
        assert_false(@collection.find.to_a.empty?, "wire_version:#{wire_version}")
        assert_equal [{"a"=>1, "x"=>2}, {"a"=>2, "x"=>4}, {"x"=>3}, {"x"=>3}, {"x"=>4}], sort_docs(@collection.find.to_a.collect { |doc| doc.delete("_id"); doc })
      end
    end

    should "running with w 0 should not report write errors" do
      with_write_commands_and_operations(@db.connection) do
        @bulk.insert({:_id => 1, :a => 1 })
        @bulk.insert({:_id => 1, :a => 2 })
        @bulk.execute({:w => 0}) # should raise no duplicate key error
      end
    end

    # ----- W > 0 WITH STANDALONE -----

    should "disallow w > 0 against a standalone" do
      with_write_commands_and_operations(@db.connection) do |wire_version|
        @collection.remove
        @bulk.insert({:_id => 1, :a => 1 })
        @bulk.insert({:_id => 2, :a => 1 })
        @bulk.insert({:_id => 3, :a => 1 })
        assert_raise_error BulkWriteError do
          @bulk.execute({:w => 2})
        end
        assert (@collection.count == batch_commands?(wire_version) ? 0 : 1)
      end
    end

  end
end
