/*
 * Copyright (c) 2008-2015 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.
 */

package com.mongodb.acceptancetest.atomicoperations;

import com.mongodb.client.DatabaseTestCase;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.test.Worker;
import com.mongodb.client.test.WorkerCodecProvider;
import org.bson.Document;
import org.bson.codecs.DocumentCodecProvider;
import org.bson.codecs.ValueCodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.ObjectId;
import org.junit.Test;

import java.util.Date;

import static java.util.Arrays.asList;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;

/**
 * Documents and tests the functionality provided for find-and-replace atomic operations.
 *
 * @mongodb.driver.manual reference/command/findAndModify/ Find and Modify
 */
public class FindAndReplaceAcceptanceTest extends DatabaseTestCase {
    private static final String KEY = "searchKey";
    private static final String VALUE_TO_CARE_ABOUT = "Value to match";

    @Test
    public void shouldReplaceDocumentAndReturnOriginal() {
        Document documentInserted = new Document(KEY, VALUE_TO_CARE_ABOUT).append("someOtherField", "withSomeOtherValue");
        collection.insertOne(documentInserted);

        assertThat(collection.count(), is(1L));

        Document document = collection.findOneAndReplace(new Document(KEY, VALUE_TO_CARE_ABOUT),
                                                         new Document("foo", "bar").append("_id", documentInserted.get("_id")));

        assertThat("Document, retrieved from replaceAndGet should match the document inserted before",
                   document, equalTo(documentInserted));
    }

    @Test
    public void shouldReplaceAndReturnOriginalItemWithDocumentRequiringACustomEncoder() {
        Worker pat = new Worker(new ObjectId(), "Pat", "Sales", new Date(), 0);
        CodecRegistry codecRegistry = fromProviders(asList(new ValueCodecProvider(), new DocumentCodecProvider(),
                new WorkerCodecProvider()));
        MongoCollection<Worker> collection = database
                .getCollection(getCollectionName(), Worker.class)
                .withCodecRegistry(codecRegistry);

        collection.insertOne(pat);

        assertThat(collection.count(), is(1L));

        Worker jordan = new Worker(pat.getId(), "Jordan", "Engineer", new Date(), 1);
        Worker returnedDocument = collection.findOneAndReplace(new Document("name", "Pat"), jordan);

        assertThat("Document, retrieved from getOneAndReplace, should match the document inserted before",
                   returnedDocument, equalTo(pat));
    }

    @Test
    public void shouldReplaceAndReturnNewItemWithDocumentRequiringACustomEncoder() {
        Worker pat = new Worker(new ObjectId(), "Pat", "Sales", new Date(), 3);
        CodecRegistry codecRegistry = fromProviders(asList(new ValueCodecProvider(), new DocumentCodecProvider(),
                new WorkerCodecProvider()));
        MongoCollection<Worker> collection = database
                .getCollection(getCollectionName(), Worker.class)
                .withCodecRegistry(codecRegistry);
        collection.insertOne(pat);

        assertThat(collection.count(), is(1L));

        Worker jordan = new Worker(pat.getId(), "Jordan", "Engineer", new Date(), 7);
        Worker returnedDocument = collection.findOneAndReplace(new Document("name", "Pat"), jordan,
                                                               new FindOneAndReplaceOptions().returnDocument(ReturnDocument.AFTER));

        assertThat("Worker retrieved from replaceOneAndGet should match the updated Worker",
                   returnedDocument, equalTo(jordan));
    }

    @Test
    public void shouldReturnNewDocumentAfterReplaceWhenUsingReplaceOneAndGet() {
        ObjectId id = new ObjectId();
        Document documentInserted = new Document("_id", id).append(KEY, VALUE_TO_CARE_ABOUT);
        Document documentReplacement = new Document("_id", id).append("foo", "bar");
        collection.insertOne(documentInserted);

        assertThat(collection.count(), is(1L));

        Document document = collection.findOneAndReplace(new Document(KEY, VALUE_TO_CARE_ABOUT), documentReplacement,
                                                         new FindOneAndReplaceOptions().returnDocument(ReturnDocument.AFTER));


        assertThat("Document, retrieved from replaceAndGet after change applied should match the document used as replacement",
                   document, equalTo(documentReplacement));
    }

    @Test
    public void shouldReturnNullWhenNothingToReplaceForGetOneAndReplace() {
        Document documentInserted = new Document(KEY, VALUE_TO_CARE_ABOUT);
        collection.insertOne(documentInserted);

        assertThat(collection.count(), is(1L));

        Document document = collection.findOneAndReplace(new Document(KEY, "bar"), new Document("foo", "bar"));

        assertNull("Document retrieved from getOneAndReplace should be null when no matching document found", document);
    }

    @Test
    public void shouldReturnNullWhenNothingToReplaceForReplaceOneAndGet() {
        Document documentInserted = new Document(KEY, VALUE_TO_CARE_ABOUT);
        collection.insertOne(documentInserted);

        assertThat(collection.count(), is(1L));

        Document document = collection.findOneAndReplace(new Document(KEY, "bar"), new Document("foo", "bar"));

        assertNull("Document retrieved from replaceOneAndGet should be null when no matching document found", document);
    }

    @Test
    public void shouldInsertDocumentWhenFilterDoesNotMatchAnyDocumentsAndUpsertFlagIsSet() {
        Document originalDocument = new Document(KEY, VALUE_TO_CARE_ABOUT);
        collection.insertOne(originalDocument);

        assertThat(collection.count(), is(1L));

        Document replacementDocument = new Document("_id", new ObjectId()).append("foo", "bar");

        Document document = collection.findOneAndReplace(new Document(KEY, "valueThatDoesNotMatch"),
                                                         replacementDocument,
                                                         new FindOneAndReplaceOptions()
                                                             .upsert(true)
                                                             .returnDocument(ReturnDocument.AFTER));

        assertThat(collection.count(), is(2L));
        assertThat("Document retrieved from replaceOneAndGet with filter that doesn't match should match the replacement document",
                   document, equalTo(replacementDocument));
    }
}
