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

import com.mongodb.Block;
import com.mongodb.Function;
import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.client.DatabaseTestCase;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.RenameCollectionOptions;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonRegularExpression;
import org.bson.BsonString;
import org.bson.BsonTimestamp;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWithScope;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.junit.Ignore;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

/**
 * Documents the basic functionality of MongoDB Collections available via the Java driver.
 */
public class CollectionAcceptanceTest extends DatabaseTestCase {

    @Test
    public void shouldBeAbleToIterateOverACollection() {
        int numberOfDocuments = 10;
        initialiseCollectionWithDocuments(numberOfDocuments);

        int countOfDocumentsInIterator = 0;
        for (final Document document : collection.find()) {
            assertThat(document, is(notNullValue()));
            countOfDocumentsInIterator++;
        }
        assertThat(countOfDocumentsInIterator, is(numberOfDocuments));
    }

    @Test
    public void shouldBeAbleToIterateOverACursor() {
        int numberOfDocuments = 10;
        initialiseCollectionWithDocuments(numberOfDocuments);

        MongoCursor<Document> cursor = collection.find().iterator();
        int countOfDocumentsInIterator = 0;
        try {
            while (cursor.hasNext()) {
                assertThat(cursor.next(), is(notNullValue()));
                countOfDocumentsInIterator++;
            }
        } finally {
            cursor.close();
        }
        assertThat(countOfDocumentsInIterator, is(numberOfDocuments));
    }

    @Test
    public void shouldCountNumberOfDocumentsInCollection() {
        assertThat(collection.count(), is(0L));

        collection.insertOne(new Document("myField", "myValue"));

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

    @Test
    public void shouldDropExistingCollection() {
        String collectionName = "shouldDropExistingCollection";
        database.createCollection(collectionName);
        MongoCollection<Document> newCollection = database.getCollection(collectionName);

        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(collectionName), is(true));

        newCollection.drop();

        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(collectionName), is(false));
    }

    @Test
    public void shouldAcceptDocumentsWithAllValidValueTypes() {
        Document doc = new Document();
        doc.append("_id", new ObjectId());
        doc.append("bool", true);
        doc.append("int", 3);
        doc.append("long", 5L);
        doc.append("str", "Hello MongoDB");
        doc.append("double", 1.1);
        doc.append("date", new Date());
        doc.append("ts", new BsonTimestamp(5, 1));
        doc.append("pattern", new BsonRegularExpression("abc"));
        doc.append("minKey", new MinKey());
        doc.append("maxKey", new MaxKey());
        doc.append("js", new Code("code"));
        doc.append("jsWithScope", new CodeWithScope("code", new Document()));
        doc.append("null", null);
        doc.append("binary", new Binary((byte) 42, new byte[]{10, 11, 12}));
        doc.append("list", Arrays.asList(7, 8, 9));
        doc.append("doc list", Arrays.asList(new Document("x", 1), new Document("x", 2)));

        collection.insertOne(doc);
        Document found = collection.find().first();
        assertNotNull(found);
        assertEquals(ObjectId.class, found.get("_id").getClass());
        assertEquals(Boolean.class, found.get("bool").getClass());
        assertEquals(Integer.class, found.get("int").getClass());
        assertEquals(Long.class, found.get("long").getClass());
        assertEquals(String.class, found.get("str").getClass());
        assertEquals(Double.class, found.get("double").getClass());
        assertEquals(Date.class, found.get("date").getClass());
        assertEquals(BsonTimestamp.class, found.get("ts").getClass());
        assertEquals(BsonRegularExpression.class, found.get("pattern").getClass());
        assertEquals(MinKey.class, found.get("minKey").getClass());
        assertEquals(MaxKey.class, found.get("maxKey").getClass());
        assertEquals(Code.class, found.get("js").getClass());
        assertEquals(CodeWithScope.class, found.get("jsWithScope").getClass());
        assertNull(found.get("null"));
        assertEquals(Binary.class, found.get("binary").getClass());
        assertTrue(found.get("list") instanceof List);
        assertTrue(found.get("doc list") instanceof List);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldRejectDocumentsWithFieldNamesContainingDots() {
        collection.insertOne(new Document("x.y", 1));
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldRejectNestedDocumentsWithFieldNamesContainingDots() {
        collection.insertOne(new Document("x", new Document("a.b", 1)));
    }

    @Test
    public void shouldIterateOverAllDocumentsInCollection() {
        initialiseCollectionWithDocuments(10);
        List<Document> iteratedDocuments = new ArrayList<Document>();
        for (final Document cur : collection.find()) {
            iteratedDocuments.add(cur);
        }
        assertEquals(10, iteratedDocuments.size());
    }

    @Test
    public void shouldForEachOverAllDocumentsInCollection() {
        initialiseCollectionWithDocuments(10);
        final List<Document> iteratedDocuments = new ArrayList<Document>();
        collection.find().forEach(new Block<Document>() {
            @Override
            public void apply(final Document document) {
                iteratedDocuments.add(document);
            }
        });
        assertEquals(10, iteratedDocuments.size());
    }


    @Test
    public void shouldAddAllDocumentsIntoListWhenUsingFind() {
        initialiseCollectionWithDocuments(10);
        List<Document> iteratedDocuments = collection.find().into(new ArrayList<Document>());
        assertEquals(10, iteratedDocuments.size());
    }

    @Test
    public void shouldMapAllDocumentsIntoListWhenUsingFind() {
        initialiseCollectionWithDocuments(5);
        List<String> iteratedDocuments = collection.find().map(new Function<Document, String>() {
            @Override
            public String apply(final Document document) {
                return document.getInteger("_id").toString();
            }
        }).into(new ArrayList<String>());
        Collections.sort(iteratedDocuments);
        assertEquals(asList("0", "1", "2", "3", "4"), iteratedDocuments);
    }

    @Test
    public void shouldSortDocumentsWhenUsingAggregate() {
        List<Document> documents = insertAggregationTestDocuments();
        List<Document> sorted = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)))).into(new ArrayList<Document>());
        assertEquals(documents, sorted);
    }

    @Test
    public void shouldSkipDocumentsWhenUsingAggregate() {
        List<Document> documents = insertAggregationTestDocuments();
        List<Document> skipped = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)),
                                                             new Document("$skip", 1))).into(new ArrayList<Document>());
        assertEquals(documents.subList(1, 3), skipped);
    }

    @Test
    public void shouldLimitDocumentsWhenUsingAggregate() {
        List<Document> documents = insertAggregationTestDocuments();
        List<Document> limited = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)),
                                                             new Document("$limit", 2))).into(new ArrayList<Document>());
        assertEquals(documents.subList(0, 2), limited);
    }

    @Test
    public void shouldFindDocumentsWhenUsingAggregate() {
        List<Document> documents = insertAggregationTestDocuments();
        List<Document> matched = collection.aggregate(asList(new Document("$match", new Document("_id", "10012"))))
                                           .into(new ArrayList<Document>());
        assertEquals(documents.subList(1, 2), matched);
    }

    @Test
    public void shouldProjectDocumentsWhenUsingAggregate() {
        insertAggregationTestDocuments();
        List<Document> sorted = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)),
                                                            new Document("$project", new Document("_id", 0).append("zip", "$_id")))
                                                    ).into(new ArrayList<Document>());
        assertEquals(asList(new Document("zip", "01778"), new Document("zip", "10012"), new Document("zip", "94301")), sorted);
    }

    @Test
    public void shouldUnwindDocumentsWhenUsingAggregate() {
        insertAggregationTestDocuments();
        List<Document> unwound = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)),
                                                             new Document("$project", new Document("_id", 0).append("tags", 1)),
                                                             new Document("$unwind", "$tags"))
                                                     ).into(new ArrayList<Document>());
        assertEquals(asList(new Document("tags", "driver"),
                            new Document("tags", "driver"),
                            new Document("tags", "SA"),
                            new Document("tags", "CE"),
                            new Document("tags", "kernel"),
                            new Document("tags", "driver"),
                            new Document("tags", "SA"),
                            new Document("tags", "CE")),
                     unwound);
    }

    @Test
    public void shouldGroupDocumentsWhenUsingAggregate() {
        insertAggregationTestDocuments();
        List<Document> grouped = collection.aggregate(asList(new Document("$sort", new Document("_id", 1)),
                                                             new Document("$project", new Document("_id", 0).append("tags", 1)),
                                                             new Document("$unwind", "$tags"),
                                                             new Document("$group", new Document("_id", "$tags")),
                                                             new Document("$sort", new Document("_id", 1))))
                                           .into(new ArrayList<Document>());
        assertEquals(asList(new Document("_id", "CE"),
                            new Document("_id", "SA"),
                            new Document("_id", "driver"),
                            new Document("_id", "kernel")),
                     grouped);
    }

    @SuppressWarnings("unchecked")
    @Test
    public void shouldBeAbleToUseBsonValueToDistinctDocumentsOfVaryingTypes() {
        List<Object> mixedList = new ArrayList<Object>();
        mixedList.add(2);
        mixedList.add("d");
        mixedList.add(new Document("e", 3));

        collection.drop();
        collection.insertMany(asList(new Document("id", "a"), new Document("id", 1),
                                     new Document("id", new Document("b", "c")),
                                     new Document("id", new Document("list", mixedList))));

        List<BsonValue> distinct = collection.distinct("id", BsonValue.class).into(new ArrayList<BsonValue>());
        assertTrue(distinct.containsAll(asList(new BsonString("a"), new BsonInt32(1), new BsonDocument("b", new BsonString("c")),
                new BsonDocument("list", new BsonArray(asList(new BsonInt32(2), new BsonString("d"),
                        new BsonDocument("e", new BsonInt32(3))))))));

        distinct = collection.distinct("id", new Document("id", new Document("$ne", 1)), BsonValue.class).into(new ArrayList<BsonValue>());
        assertTrue(distinct.containsAll(asList(new BsonString("a"), new BsonDocument("b", new BsonString("c")),
                new BsonDocument("list", new BsonArray(asList(new BsonInt32(2), new BsonString("d"),
                        new BsonDocument("e", new BsonInt32(3))))))));
    }

    private void initialiseCollectionWithDocuments(final int numberOfDocuments) {
        MongoCollection<Document> collection = database.getCollection(getCollectionName()).withWriteConcern(WriteConcern.ACKNOWLEDGED);
        for (int i = 0; i < numberOfDocuments; i++) {
            collection.insertOne(new Document("_id", i));
        }
    }

    public List<Document> insertAggregationTestDocuments() {
        List<Document> documents = new ArrayList<Document>();
        documents.add(new Document("_id", "01778").append("city", "WAYLAND").append("state", "MA").append("population", 13100)
                                                  .append("loc", asList(42.3635, 71.3619)).append("tags", asList("driver")));
        documents.add(new Document("_id", "10012").append("city", "NEW YORK CITY")
                                                  .append("state", "NY")
                                                  .append("population", 8245000)
                                                  .append("loc", asList(40.7260, 71.3619))
                                                  .append("tags", asList("driver", "SA", "CE", "kernel")));
        documents.add(new Document("_id", "94301").append("city", "PALO ALTO")
                                                  .append("state", "CA")
                                                  .append("population", 65412)
                                                  .append("loc", asList(37.4419, 122.1419))
                                                  .append("tags", asList("driver", "SA", "CE")));

        List<Document> shuffledDocuments = new ArrayList<Document>(documents);
        Collections.shuffle(shuffledDocuments);
        collection.insertMany(shuffledDocuments);
        return documents;
    }


    @Test
    public void shouldChangeACollectionNameWhenRenameIsCalled() {
        //given
        collection.insertOne(new Document("someKey", "someValue"));

        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(getCollectionName()), is(true));

        //when
        String newCollectionName = "TheNewCollectionName";
        collection.renameCollection(new MongoNamespace(getDatabaseName(), newCollectionName));

        //then
        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(getCollectionName()), is(false));
        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(newCollectionName), is(true));

        MongoCollection<Document> renamedCollection = database.getCollection(newCollectionName);
        assertThat("Renamed collection should have the same number of documents as original",
                   renamedCollection.count(), is(1L));
    }

    @Test
    public void shouldBeAbleToRenameCollectionToAnExistingCollectionNameAndReplaceItWhenDropIsTrue() {
        //given
        String existingCollectionName = "anExistingCollection";

        String keyInOriginalCollection = "someKey";
        String valueInOriginalCollection = "someValue";
        collection.insertOne(new Document(keyInOriginalCollection, valueInOriginalCollection));

        MongoCollection<Document> existingCollection = database.getCollection(existingCollectionName);
        String keyInExistingCollection = "aDifferentDocument";
        String valueInExistingCollection = "withADifferentValue";
        existingCollection.insertOne(new Document(keyInExistingCollection, valueInExistingCollection));

        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(getCollectionName()), is(true));
        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(existingCollectionName), is(true));

        //when
        collection.renameCollection(new MongoNamespace(getDatabaseName(), existingCollectionName),
                                    new RenameCollectionOptions().dropTarget(true));

        //then
        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(getCollectionName()), is(false));
        assertThat(database.listCollectionNames().into(new ArrayList<String>()).contains(existingCollectionName), is(true));

        MongoCollection<Document> replacedCollection = database.getCollection(existingCollectionName);
        assertThat(replacedCollection.find().first().get(keyInExistingCollection), is(nullValue()));
        assertThat(replacedCollection.find().first().get(keyInOriginalCollection).toString(), is(valueInOriginalCollection));
    }

    @Test
    @Ignore("not implemented")
    public void shouldFailRenameIfSharded() {

    }

}
