#  Copyright 2015 - present 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.

use strict;
use warnings;
use Test::More 0.96;
use Test::Fatal;
use Test::Deep qw/!blessed/;

use utf8;
use Tie::IxHash;

use MongoDB;
use MongoDB::Error;

use lib "t/lib";
use MongoDBTest qw/skip_unless_mongod build_client get_test_db server_version server_type get_capped/;

skip_unless_mongod();

my $conn           = build_client();
my $testdb         = get_test_db($conn);
my $server_version = server_version($conn);
my $server_type    = server_type($conn);
my $coll           = $testdb->get_collection('test_collection');

my $supports_collation = $server_version >= 3.3.9;
my $case_insensitive_collation = { locale => "en_US", strength => 2 };

my $res;

subtest "insert_one" => sub {

    # insert doc with _id
    $coll->drop;
    $res = $coll->insert_one( { _id => "foo", value => "bar" } );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => str("foo"), value => str("bar") } ),
        "insert with _id: doc inserted"
    );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::InsertOneResult", "result" );
    is( $res->inserted_id, "foo", "res->inserted_id" );

    # insert doc without _id
    $coll->drop;
    my $orig = { value => "bar" };
    my $doc = { %$orig };
    $res = $coll->insert_one( $doc );
    my @got = $coll->find( {} )->all;
    cmp_deeply(
        \@got,
        bag( { _id => ignore(), value => str("bar") } ),
        "insert without _id: hash doc inserted"
    );
    ok( $res->acknowledged, "result acknowledged" );
    is( $got[0]{_id}, $res->inserted_id, "doc has expected inserted _id" );
    cmp_deeply( $doc, $orig, "original unmodified" );

    # insert arrayref
    $coll->drop;
    $res = $coll->insert_one( [ value => "bar" ] );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), value => str("bar") } ),
        "insert without _id: array doc inserted"
    );

    # insert Tie::Ixhash
    $coll->drop;
    $res = $coll->insert_one( Tie::IxHash->new( value => "bar" ) );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), value => str("bar") } ),
        "insert without _id: Tie::IxHash doc inserted"
    );

};

subtest "insert_many" => sub {

    # insert docs with mixed _id and not and mixed types
    $coll->drop;
    my $doc = { value => "baz" };
    $res =
      $coll->insert_many( [ [ _id => "foo", value => "bar" ], $doc, ] );
    my @got = $coll->find( {} )->all;
    cmp_deeply(
        \@got,
        bag( { _id => str("foo"), value => str("bar") }, { _id => ignore(), value => str("baz") }, ),
        "insert many: docs inserted"
    );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::InsertManyResult", "result" );
    cmp_deeply(
        $res->inserted,
        [ { index => num(0), _id => str("foo") }, { index => num(1), _id => obj_isa("BSON::OID") } ],
        "inserted contains correct hashrefs"
    );
    cmp_deeply(
        $res->inserted_ids,
        {
            0 => str("foo"),
            1 => $res->inserted->[1]{_id},
        },
        "inserted_ids contains correct keys/values"
    );
    is($res->inserted_count, 2, "Two docs inserted.");

    # ordered insert should halt on error
    $coll->drop;
    my $err = exception {
        $coll->insert_many( [ { _id => 0 }, { _id => 1 }, { _id => 2 }, { _id => 1 }, ] )
    };
    ok( $err, "ordered insert got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;
    $res = $err->result;
    is( $res->inserted_count, 3, "only first three inserted" );

    # unordered insert should not halt on error
    $coll->drop;
    $err = exception {
        $coll->insert_many( [ { _id => 0 }, { _id => 1 }, { _id => 1 }, { _id => 2 }, ], { ordered => 0 } )
    };
    ok( $err, "unordered insert got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;
    $res = $err->result;
    is( $res->inserted_count, 3, "all valid docs inserted" );

    # insert bad type
    $err = exception { $coll->insert_many( { x => 1 } ) };
    like( $err, qr/must be an array reference/, "exception inserting bad type" );
};

subtest "delete_one" => sub {
    $coll->drop;
    $coll->insert_many( [ map { { _id => $_, x => "foo" } } 1 .. 2 ] );
    is( $coll->count_documents( { x => 'foo' } ), 2, "inserted two docs" );
    $res = $coll->delete_one( { x => 'foo' } );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::DeleteResult", "result" );
    is( $res->deleted_count, 1, "delete one document" );
    is( $coll->count_documents( { x => 'foo' } ), 1, "one document left" );
    $res = $coll->delete_one( { x => 'bar' } );
    is( $res->deleted_count, 0, "delete non existent document does nothing" );
    is( $coll->count_documents( { x => 'foo' } ), 1, "one document left" );

    if ($supports_collation) {
        my $doc;
        $doc = $coll->find_one( { x => "foo" } );
        $res =
          $coll->delete_one( { x => 'FOO' }, { collation => $case_insensitive_collation } );
        is( $res->deleted_count, 1, "delete_one with collation" );
        is( $coll->count_documents( { x => 'foo' } ), 0, "no documents left" );

        my $coll2 = $coll->clone( write_concern => { w => 0 } );
        like(
            exception {
                $coll2->delete_one( { x => 'FOO' }, { collation => $case_insensitive_collation } );
            },
            qr/Unacknowledged deletes that specify a collation are not allowed/,
            "delete_one w/ collation returns error if write is unacknowledged"
        );
    }
    else {
        like(
            exception {
              $coll->delete_one( { x => 'FOO' }, { collation => $case_insensitive_collation } );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "delete_one w/ collation returns error if unsupported"
        );
    }

    # test errors -- deletion invalid on capped collection
    my $cap = get_capped($testdb);
    $cap->insert_many( [ map { { _id => $_ } } 1..10 ] );
    my $err = exception { $cap->delete_one( { _id => 4 } ) };
    ok( $err, "deleting from capped collection throws error" );
    isa_ok( $err, 'MongoDB::WriteError' );
    like( $err->result->last_errmsg, qr/capped/, "error had string 'capped'" );
};

subtest "delete_many" => sub {
    $coll->drop;
    $coll->insert_many( [ map { { _id => $_, x => "foo" } } 1 .. 5 ] );
    is( $coll->count_documents( {} ), 5, "inserted five docs" );
    $res = $coll->delete_many( { _id => { '$gt', 3 } } );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::DeleteResult", "result" );
    is( $res->deleted_count, 2, "deleted two documents" );
    is( $coll->count_documents( {} ), 3, "three documents left" );
    $res = $coll->delete_many( { y => 'bar' } );
    is( $res->deleted_count, 0, "delete non existent document does nothing" );
    is( $coll->count_documents( {} ), 3, "three documents left" );

    if ($supports_collation) {
        $res =
          $coll->delete_many( { x => 'FOO' }, { collation => $case_insensitive_collation } );
        is( $res->deleted_count, 3, "delete_many with collation" );
        is( $coll->count_documents( {} ), 0, "no documents left" );
    }
    else {
        like(
            exception {
                $coll->delete_many( { x => 'FOO' }, { collation => $case_insensitive_collation } );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "delete_many w/ collation returns error if unsupported"
        );
    }

    # test errors -- deletion invalid on capped collection
    my $cap = get_capped($testdb);
    $cap->insert_many( [ map { { _id => $_ } } 1..10 ] );
    my $err = exception { $cap->delete_many( {} ) };
    ok( $err, "deleting from capped collection throws error" );
    isa_ok( $err, 'MongoDB::WriteError' );
    like( $err->result->last_errmsg, qr/capped/, "error had string 'capped'" );
};

subtest "replace_one" => sub {
    $coll->drop;

    # replace missing doc without upsert
    $res = $coll->replace_one( { x => 1 }, { x => 2 } );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::UpdateResult", "result" );
    is( $res->matched_count, 0, "matched count is zero" );
    is( $coll->count_documents( {} ), 0, "collection still empty" );

    # replace missing with upsert
    $res = $coll->replace_one( { x => 1 }, { x => 2 }, { upsert => 1 } );
    is( $res->matched_count, 0, "matched count is zero" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 0 : undef ),
        "modified count correct based on server version"
    );

    ok( $server_version >= 2.6.0 ? $res->has_modified_count : !$res->has_modified_count,
        "has_modified_count correct" );

    isa_ok( $res->upserted_id, "BSON::OID", "got upserted id" );
    is( $coll->count_documents( {} ), 1, "one doc in database" );
    my $got = $coll->find_one( { _id => $res->upserted_id } );
    is( $got->{x}, 2, "document contents correct" );

    # replace existing with upsert -- add duplicate to confirm only one
    $coll->insert_one( { x => 2 } );
    $res = $coll->replace_one( { x => 2 }, { x => 3 }, { upsert => 1 } );
    is( $coll->count_documents( {} ), 2, "replace existing with upsert" );
    is( $res->matched_count, 1, "matched_count 1" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 1 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(2) }, { _id => ignore, x => num(3) } ),
        "collection docs correct"
    );

    # replace existing without upsert
    $res = $coll->replace_one( { x => 3 }, { x => 4 } );
    is( $coll->count_documents( {} ), 2, "replace existing with upsert" );
    is( $res->matched_count, 1, "matched_count 1" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 1 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(2) }, { _id => ignore, x => num(4) } ),
        "collection docs correct"
    );

    # replace doc with $op is an error
    my $err = exception {
        $coll->replace_one( { x => 3} , { '$set' => { x => 4 } } )
    };
    ok( $err, "replace with update operators is an error" );
    like( $err, qr/must not contain update operators/, "correct error message" );

    # replace doc with custom op_char is an error
    $err = exception {
        my $coll2 = $coll->with_codec( op_char => '-' );
        $coll2->replace_one( { x => 3} , { -set => { x => 4 } } )
    };
    ok( $err, "replace with op_char update operators is an error" );
    like( $err, qr/must not contain update operators/, "correct error message" );

    if ($supports_collation) {
        $coll->insert_one( { x => 'foo' } );

        $res = $coll->replace_one(
            { x         => 'FOO' },
            { x         => 'bar' },
            { collation => $case_insensitive_collation }
        );
        is( $coll->count_documents( { x => 'bar' } ), 1, "replace_one with collation" );
    }
    else {
        like(
            exception {
                $coll->replace_one(
                    { x         => 'FOO' },
                    { x         => 'bar' },
                    { collation => $case_insensitive_collation }
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "replace_one w/ collation returns error if unsupported"
        );
    }
};

subtest "update_one" => sub {
    $coll->drop;

    # update missing doc without upsert
    $res = $coll->update_one( { x => 1 }, { '$set' => { x => 2 } } );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::UpdateResult", "result" );
    is( $res->matched_count, 0, "matched count is zero" );
    is( $coll->count_documents( {} ), 0, "collection still empty" );

    # update missing with upsert
    $res = $coll->update_one( { x => 1 }, { '$set' => { x => 2 } }, { upsert => 1 } );
    is( $res->matched_count, 0, "matched count is zero" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 0 : undef ),
        "modified count correct based on server version"
    );
    isa_ok( $res->upserted_id, "BSON::OID", "got upserted id" );
    is( $coll->count_documents( {} ), 1, "one doc in database" );
    my $got = $coll->find_one( { _id => $res->upserted_id } );
    is( $got->{x}, 2, "document contents correct" );

    # update existing with upsert -- add duplicate to confirm only one
    $coll->insert_one( { x => 2 } );
    $res = $coll->update_one( { x => 2 }, { '$set' => { x => 3 } }, { upsert => 1 } );
    is( $coll->count_documents( {} ), 2, "update existing with upsert" );
    is( $res->matched_count, 1, "matched_count 1" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 1 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(2) }, { _id => ignore, x => num(3) } ),
        "collection docs correct"
    );

    # update existing without upsert
    $res = $coll->update_one( { x => 3 }, { '$set' => { x => 4 } } );
    is( $coll->count_documents( {} ), 2, "update existing with upsert" );
    is( $res->matched_count, 1, "matched_count 1" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 1 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(2) }, { _id => ignore, x => num(4) } ),
        "collection docs correct"
    );

    # update doc without $op is an error
    my $err = exception {
        $coll->update_one( { x => 3} , { x => 4 } )
    };
    ok( $err, "update without update operators is an error" );
    like( $err, qr/must only contain update operators/, "correct error message" );

    if ($supports_collation) {
        $coll->insert_one( { x => 'foo' } );

        $res = $coll->update_one(
            { x         => 'FOO' },
            { '$set'    => { x => 'bar' } },
            { collation => $case_insensitive_collation }
        );
        is( $coll->count_documents( { x => 'bar' } ), 1, "update_one with collation" );

        my $coll2 = $coll->clone( write_concern => { w => 0 } );
        like(
            exception {
                $coll2->update_one(
                    { x         => 'FOO' },
                    { '$set'    => { x => 'bar' } },
                    { collation => $case_insensitive_collation }
                );
            },
            qr/Unacknowledged updates that specify a collation are not allowed/,
            "update_one w/ collation returns error if write is unacknowledged"
        );
    }
    else {
        like(
            exception {
                $coll->update_one(
                    { x         => 'FOO' },
                    { '$set'    => { x => 'bar' } },
                    { collation => $case_insensitive_collation }
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "update_one w/ collation returns error if unsupported"
        );
    }
};

subtest "update_many" => sub {
    $coll->drop;

    # update missing doc without upsert
    $res = $coll->update_many( { x => 1 }, { '$set' => { x => 2 } } );
    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::UpdateResult", "result" );
    is( $res->matched_count, 0, "matched count is zero" );
    is( $coll->count_documents( {} ), 0, "collection still empty" );

    # update missing with upsert
    $res = $coll->update_many( { x => 1 }, { '$set' => { x => 2 } }, { upsert => 1 } );
    is( $res->matched_count, 0, "matched count is zero" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 0 : undef ),
        "modified count correct based on server version"
    );
    isa_ok( $res->upserted_id, "BSON::OID", "got upserted id" );
    is( $coll->count_documents( {} ), 1, "one doc in database" );
    my $got = $coll->find_one( { _id => $res->upserted_id } );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(2) } ),
        "collection docs correct"
    );

    # update existing with upsert -- add duplicate to confirm multiple
    $coll->insert_one( { x => 2 } );
    $res = $coll->update_many( { x => 2 }, { '$set' => { x => 3 } }, { upsert => 1 } );
    is( $coll->count_documents( {} ), 2, "update existing with upsert" );
    is( $res->matched_count, 2, "matched_count 2" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 2 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(3) }, { _id => ignore, x => num(3) } ),
        "collection docs correct"
    );

    # update existing without upsert
    $res = $coll->update_many( { x => 3 }, { '$set' => { x => 4 } } );
    is( $coll->count_documents( {} ), 2, "update existing with upsert" );
    is( $res->matched_count, 2, "matched_count 1" );
    is(
        $res->modified_count,
        ( $server_version >= v2.6.0 ? 2 : undef ),
        "modified count correct based on server version"
    );
    cmp_deeply(
        [ $coll->find( {} )->all ],
        bag( { _id => ignore(), x => num(4) }, { _id => ignore, x => num(4) } ),
        "collection docs correct"
    );

    # update doc without $op is an error
    my $err = exception {
        $coll->update_one( { x => 3 } , { x => 4 } )
    };
    ok( $err, "update without update operators is an error" );
    like( $err, qr/must only contain update operators/, "correct error message" );

    if ($supports_collation) {
        $coll->insert_one( { x => 'foo' } );
        $coll->insert_one( { x => 'Foo' } );

        $res = $coll->update_many(
            { x         => 'FOO' },
            { '$set'    => { x => 'bar' } },
            { collation => $case_insensitive_collation }
        );
        is( $coll->count_documents( { x => 'bar' } ), 2, "update_many with collation" );
    }
    else {
        like(
            exception {
                $coll->update_many(
                    { x         => 'FOO' },
                    { '$set'    => { x => 'bar' } },
                    { collation => $case_insensitive_collation }
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "update_many w/ collation returns error if unsupported"
        );
    }
};

subtest 'bulk_write' => sub {
    $coll->drop;

    # test mixed-form write models, array/hash refs or pairs
    $res = $coll->bulk_write(
        [
            [ insert_one  => [ { x => 1 } ] ],
            { insert_many => [ { x => 2 }, { x => 3 } ] },
            replace_one => [ { x => 1 }, { x      => 4 } ],
            update_one  => [ { x => 7 }, { '$set' => { x => 5 } }, { upsert => 1 } ],
            [ insert_one  => [ { x => 6 } ] ],
            { insert_many => [ { x => 7 }, { x => 8 } ] },
            delete_one  => [ { x => 4 } ],
            delete_many => [ { x => { '$lt' => 3 } } ],
            update_many => [ { x => { '$gt' => 5 } }, { '$inc' => { x => 1 } } ],
        ],
    );

    ok( $res->acknowledged, "result acknowledged" );
    isa_ok( $res, "MongoDB::BulkWriteResult", "result" );
    is( $res->op_count, 11, "op count correct" );

    my @got = $coll->find( {} )->all;
    cmp_deeply(
        \@got,
        bag( map { { _id => ignore, x => num($_) } } 3, 5, 7, 8, 9 ),
        "collection docs correct",
    ) or diag explain \@got;

    # test ordered error
    # ordered insert should not halt on error
    $coll->drop;
    my $err = exception {
        $coll->bulk_write(
            [
                insert_one => [ { _id => 1 } ],
                insert_one => [ { _id => 2 } ],
                insert_one => [ { _id => 1 } ],
            ],
            { ordered => 1, },
        );
    };
    ok( $err, "ordered bulk got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;
    $res = $err->result;
    is( $res->inserted_count, 2, "only first two inserted" );

    # test unordered error
    # unordered insert should halt on error
    $coll->drop;
    $err = exception {
        $coll->bulk_write(
            [
                insert_one => [ { _id => 1 } ],
                insert_one => [ { _id => 2 } ],
                insert_one => [ { _id => 1 } ],
                insert_one => [ { _id => 3 } ],
            ],
            { ordered => 0, },
        );
    };
    ok( $err, "unordered bulk got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;
    $res = $err->result;
    is( $res->inserted_count, 3, "three valid docs inserted" );

    # test collation
    $coll->drop;
    $coll->insert_one( { x => $_, y => 1 } ) for "a" .. "e";
    $err = exception {
        $coll->bulk_write(
            [
                update_one => [
                    { x         => "A" },
                    { '$set'    => { y => 0 } },
                    { collation => $case_insensitive_collation }
                ],
                update_many => [
                    { x         => "B" },
                    { '$set'    => { y => 0 } },
                    { collation => $case_insensitive_collation }
                ],
                replace_one =>
                  [ { x => "C" }, { y => 0 }, { collation => $case_insensitive_collation } ],
                delete_one  => [ { x => "D" }, { collation => $case_insensitive_collation } ],
                delete_many => [ { x => "E" }, { collation => $case_insensitive_collation } ],
            ]
        );
    };
    if ($supports_collation) {
        is( $err, undef, "bulk_write w/ collation" );
        is( $coll->count_documents( { y => 1 } ), 0, "collection updated" );
    }
    else {
        like(
            $err,
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "bulk_write w/ collation returns error if unsupported"
        );
        is( $coll->count_documents( { y => 1 } ), 5, "collection not updated" );
    }
};

subtest "find_one_and_delete" => sub {
    $coll->drop;
    $coll->insert_one( { x => 1, y => 'a' } );
    $coll->insert_one( { x => 1, y => 'b' } );
    is( $coll->count_documents( {} ), 2, "inserted 2 docs" );

    my $doc;

    # find non-existent doc
    $doc = $coll->find_one_and_delete( { x => 2 } );
    is( $doc, undef, "find_one_and_delete on nonexistent doc returns undef" );
    is( $coll->count_documents( {} ), 2, "still 2 docs" );

    # find/remove existing doc (testing sort and projection, too)
    $doc = $coll->find_one_and_delete( { x => 1 },
        { sort => [ y => 1 ], projection => { y => 1 } } );
    cmp_deeply( $doc, { _id => ignore(), y => str("a") }, "expected doc returned" );
    is( $coll->count_documents( {} ), 1, "only 1 doc left" );

    if ($supports_collation) {
        $doc = $coll->find_one_and_delete(
            { y         => 'B' },
            { collation => $case_insensitive_collation },
        );
        cmp_deeply(
            $doc,
            { _id => ignore(), x => num(1), y => str("b") },
            "find_one_and_delete with collation"
        );
        is( $coll->count_documents( { y => 'b' } ), 0, "no documents left" );
    }
    else {
        like(
            exception {
                $coll->find_one_and_delete(
                    { y         => 'B' },
                    { collation => $case_insensitive_collation },
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "find_one_and_delete w/ collation returns error if unsupported"
        );
    }

    # XXX how to test max_time_ms?
};

subtest "find_one_and_replace" => sub {
    $coll->drop;
    $coll->insert_one( { x => 1, y => 'a' } );
    $coll->insert_one( { x => 1, y => 'b' } );
    is( $coll->count_documents( {} ), 2, "inserted 2 docs" );

    my $doc;

    # find and replace non-existent doc, without upsert
    $doc = $coll->find_one_and_replace( { x => 2 }, { x => 3, y => 'c' } );
    is( $doc, undef, "find_one_and_replace on nonexistent doc returns undef" );
    is( $coll->count_documents( {} ), 2, "still 2 docs" );
    is( $coll->count_documents( { x => 3 } ), 0, "no docs matching replacment" );

    # find and replace non-existent doc, with upsert
    $doc = $coll->find_one_and_replace( { x => 2 }, { x => 3, y => 'c' }, { upsert => 1 } );
    if ( $server_version >= v2.2.0 ) {
        is( $doc, undef, "find_one_and_replace upsert on nonexistent doc returns undef" );
    }
    is( $coll->count_documents( {} ), 3, "doc has been upserted" );
    is( $coll->count_documents( { x => 3 } ), 1, "1 doc matching replacment" );

    # find and replace existing doc, with upsert
    $doc = $coll->find_one_and_replace( { x => 3 }, { x => 4, y => 'c' }, { upsert => 1 });
    cmp_deeply(
        $doc,
        { _id => ignore(), x => num(3), y => str("c") },
        "find_one_and_replace on existing doc returned old doc",
    );
    is( $coll->count_documents( {} ), 3, "no new doc added" );
    is( $coll->count_documents( { x => 4 } ), 1, "1 doc matching replacment" );

    # find and replace existing doc, with after doc
    $doc = $coll->find_one_and_replace( { x => 4 }, { x => 5, y => 'c' }, { returnDocument => 'after' });
    cmp_deeply(
        $doc,
        { _id => ignore(), x => num(5), y => str("c") },
        "find_one_and_replace on existing doc returned new doc",
    );
    is( $coll->count_documents( {} ), 3, "no new doc added" );
    is( $coll->count_documents( { x => 5 } ), 1, "1 doc matching replacment" );

    # test project and sort
    $doc = $coll->find_one_and_replace( { x => 1 }, { x => 2, y => 'z' }, { sort => [ y => -1 ], projection => { y => 1 } } );
    cmp_deeply(
        $doc,
        { _id => ignore(), y => str("b") },
        "find_one_and_replace on existing doc returned new doc",
    );
    is( $coll->count_documents( { x => 2 } ), 1, "1 doc matching replacment" );
    is( $coll->count_documents( { x => 1, y => 'a' } ), 1, "correct doc untouched" );

    # test duplicate key error
    $coll->drop;
    $coll->insert_many( [ map { { _id => $_ } } 0 .. 2 ] );
    my $err = exception {
        $coll->find_one_and_replace( { x => 1 }, { _id => 0 }, { upsert => 1 } );
    };
    ok( $err, "upsert dup key got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;

    $coll->drop;
    if ($supports_collation) {
        $coll->insert_one( { y => 'b' } );

        $doc = $coll->find_one_and_replace(
            { y         => 'B' },
            { y         => 'c' },
            { collation => $case_insensitive_collation },
        );
        cmp_deeply(
            $doc,
            { _id => ignore(), y => str("b") },
            "find_one_and_replace with collation"
        );
        is( $coll->count_documents( { y => 'c' } ), 1, "doc matching replacement" );
    }
    else {
        like(
            exception {
                $coll->find_one_and_replace(
                    { y         => 'B' },
                    { y         => 'c' },
                    { collation => $case_insensitive_collation },
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "find_one_and_replace w/ collation returns error if unsupported"
        );
    }
};

subtest "find_one_and_update" => sub {
    $coll->drop;
    $coll->insert_one( { x => 1, y => 'a' } );
    $coll->insert_one( { x => 1, y => 'b' } );
    is( $coll->count_documents( {} ), 2, "inserted 2 docs" );

    my $doc;

    # find and update non-existent doc, without upsert
    $doc = $coll->find_one_and_update( { x => 2 }, { '$inc' => { x => 1 } } );
    is( $doc, undef, "find_one_and_update on nonexistent doc returns undef" );
    is( $coll->count_documents( {} ), 2, "still 2 docs" );
    is( $coll->count_documents( { x => 3 } ), 0, "no docs matching update" );

    # find and update non-existent doc, with upsert
    $doc = $coll->find_one_and_update( { x => 2 }, { '$inc' => { x => 1 }, '$set' => { y => 'c' } }, { upsert => 1 } );
    if ( $server_version >= v2.2.0 ) {
        is( $doc, undef, "find_one_and_update upsert on nonexistent doc returns undef" );
    }
    is( $coll->count_documents( {} ), 3, "doc has been upserted" );
    is( $coll->count_documents( { x => 3 } ), 1, "1 doc matching upsert" );

    # find and update existing doc, with upsert
    $doc = $coll->find_one_and_update( { x => 3 }, { '$inc' => { x => 1 } }, { upsert => 1 });
    cmp_deeply(
        $doc,
        { _id => ignore(), x => num(3), y => str("c") },
        "find_one_and_update on existing doc returned old doc",
    );
    is( $coll->count_documents( {} ), 3, "no new doc added" );
    is( $coll->count_documents( { x => 4 } ), 1, "1 doc matching replacment" );

    # find and update existing doc, with after doc
    $doc = $coll->find_one_and_update( { x => 4 }, { '$inc' => { x => 1 } }, { returnDocument => 'after' });
    cmp_deeply(
        $doc,
        { _id => ignore(), x => num(5), y => str("c") },
        "find_one_and_update on existing doc returned new doc",
    );
    is( $coll->count_documents( {} ), 3, "no new doc added" );
    is( $coll->count_documents( { x => 5 } ), 1, "1 doc matching replacment" );

    # test project and sort
    $doc = $coll->find_one_and_update(
        { x      => 1 },
        { '$inc' => { x => 1 }, '$set' => { y => 'z' } },
        { sort   => [ y => -1 ], projection => { y => 1 } }
    );
    cmp_deeply(
        $doc,
        { _id => ignore(), y => str("b") },
        "find_one_and_update on existing doc returned new doc",
    );
    is( $coll->count_documents( { x => 2 } ), 1, "1 doc matching replacment" );
    is( $coll->count_documents( { x => 1, y => 'a' } ), 1, "correct doc untouched" );

    # test duplicate key error
    $coll->drop;
    $coll->indexes->create_one([x => 1], {unique => 1});
    $coll->insert_many( [ map { { _id => $_, x => $_ } } 1 .. 3 ] );
    my $err = exception {
        $coll->find_one_and_update( { x => 0 }, { '$set' => { x => 1 } }, { upsert => 1 } );
    };
    ok( $err, "update dup key got an error" );
    isa_ok( $err, 'MongoDB::DuplicateKeyError', 'caught error' )
      or diag explain $err;

    $coll->drop;
    if ($supports_collation) {
        $coll->insert_one( { y => 'b' } );

        $doc = $coll->find_one_and_update(
            { y         => 'B' },
            { '$set'    => { y => 'c' } },
            { collation => $case_insensitive_collation },
        );
        cmp_deeply(
            $doc,
            { _id => ignore(), y => str("b") },
            "find_one_and_update with collation"
        );
        is( $coll->count_documents( { y => 'c' } ), 1, "doc matching replacement" );
    }
    else {
        like(
            exception {
                $coll->find_one_and_update(
                    { y         => 'B' },
                    { '$set'    => { y => 'c' } },
                    { collation => $case_insensitive_collation },
                );
            },
            qr/MongoDB host '.*:\d+' doesn't support collation/,
            "find_one_and_update w/ collation returns error if unsupported"
        );
    }
};

subtest "write concern errors" => sub {
    plan skip_all => "not a replica set"
        unless $server_type eq 'RSPrimary';

    $coll->drop;
    my $coll2 = $coll->clone( write_concern => { w => 99 } );

    my @cases = (
        [ insert_one => [ { x => 1 } ] ],
        [ insert_many => [ [ { x => 2 }, { x => 3 } ] ] ],
        [ delete_one => [ { x => 1 } ] ],
        [ delete_one => [ {} ] ],
        [ replace_one => [ { x => 0 }, { x => 1 }, { upsert => 1 } ] ],
        [ update_one => [ { x => 1 }, { '$inc' => { x => 1 } } ] ],
    );

    # findAndModify doesn't take write concern until MongoDB 3.2
    if ( $server_version >= v3.2.0 ) {
        push @cases,
          (
            [ find_one_and_replace => [ { x => 2 }, { x      => 1 } ] ],
            [ find_one_and_update  => [ { x => 1 }, { '$inc' => { x => 1 } } ] ],
            [ find_one_and_delete => [ { x => 2 } ] ],
          );
    }

    for my $c ( @cases ) {
        my ($method, $args) = @$c;
        my $res;
        my $err = exception { $res = $coll2->$method( @$args ) };
        ok( $err, "caught error for $method" ) or diag explain $res;
        isa_ok( $err, 'MongoDB::WriteConcernError', "$method error" )
            or diag explain $err;
    }
};


done_testing;
