/*
 * 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;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeThat;

public class DBCursorOldTest extends DatabaseTestCase {

    @Test
    public void testGetServerAddressLoop() {
        insertTestData(collection, 10);

        DBCursor cur = collection.find();

        while (cur.hasNext()) {
            cur.next();
            assertNotNull(cur.getServerAddress());
        }
    }

    @Test
    public void testGetServerAddressQuery() {
        insertTestData(collection, 10);

        DBCursor cur = collection.find();
        cur.hasNext();
        assertNotNull(cur.getServerAddress());
    }

    @Test
    public void testGetServerAddressQuery1() {
        insertTestData(collection, 10);

        DBCursor cur = collection.find(new BasicDBObject("x", 9));
        cur.hasNext();
        assertNotNull(cur.getServerAddress());
    }

    @Test
    public void testCount() {
        assertEquals(collection.find().count(), 0);
        BasicDBObject obj = new BasicDBObject();
        obj.put("x", "foo");
        collection.insert(obj);

        assertEquals(1, collection.find().count());
    }

    @Test
    public void testSnapshot() {
        insertTestData(collection, 100);
        assertEquals(100, collection.find().count());
        assertEquals(100, collection.find().toArray().size());
        assertEquals(100, collection.find().snapshot().count());
        assertEquals(100, collection.find().snapshot().toArray().size());
        assertEquals(100, collection.find().snapshot().limit(50).count());
        assertEquals(50, collection.find().snapshot().limit(50).toArray().size());
    }

    @Test
    public void testOptions() {
        DBCursor dbCursor = collection.find();

        assertEquals(0, dbCursor.getOptions());
        dbCursor.setOptions(Bytes.QUERYOPTION_TAILABLE);
        assertEquals(Bytes.QUERYOPTION_TAILABLE, dbCursor.getOptions());
        dbCursor.addOption(Bytes.QUERYOPTION_SLAVEOK);
        assertEquals(Bytes.QUERYOPTION_TAILABLE | Bytes.QUERYOPTION_SLAVEOK, dbCursor.getOptions());
        dbCursor.resetOptions();
        assertEquals(0, dbCursor.getOptions());
    }


    @Test
    public void testTailable() {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));

        DBObject firstDBObject = new BasicDBObject("x", 1);
        DBObject secondDBObject = new BasicDBObject("x", 2);

        DBCursor cur = c.find()
                        .sort(new BasicDBObject("$natural", 1))
                        .addOption(Bytes.QUERYOPTION_TAILABLE);
        c.save(firstDBObject, WriteConcern.ACKNOWLEDGED);

        assertEquals(firstDBObject, cur.tryNext());
        assertEquals(firstDBObject, cur.curr());
        assertEquals(1, cur.numSeen());

        assertEquals(null, cur.tryNext());
        assertEquals(firstDBObject, cur.curr());
        assertEquals(1, cur.numSeen());

        c.save(secondDBObject, WriteConcern.ACKNOWLEDGED);
        assertEquals(secondDBObject, cur.tryNext());
        assertEquals(secondDBObject, cur.curr());
        assertEquals(2, cur.numSeen());

        assertEquals(null, cur.tryNext());
        assertEquals(secondDBObject, cur.curr());
        assertEquals(2, cur.numSeen());
    }

    @Test
    public void testTailableImplicitAwaitOnHasNext() throws ExecutionException, TimeoutException, InterruptedException {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));
        for (int i = 0; i < 10; i++) {
            c.save(new BasicDBObject("x", i), WriteConcern.ACKNOWLEDGED);
        }

        final DBCursor cur = c.find()
                              .sort(new BasicDBObject("$natural", 1))
                              .addOption(Bytes.QUERYOPTION_TAILABLE);

        final CountDownLatch latch = new CountDownLatch(1);
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // the following call will block on the last hasNext
                int i = 0;
                while (cur.hasNext()) {
                    DBObject obj = cur.next();
                    i++;
                    if (i == 10) {
                        latch.countDown();
                    } else if (i > 10) {
                        return (Integer) obj.get("x");
                    }
                }

                return null;
            }
        };

        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<Integer> future = es.submit(callable);

        latch.await(5, SECONDS);

        // this doc should unblock thread
        c.save(new BasicDBObject("x", 10), WriteConcern.ACKNOWLEDGED);
        assertEquals(10, (long) future.get(5, SECONDS));
    }

    @Test
    public void testTailableImplicitAwaitOnNext() throws ExecutionException, TimeoutException, InterruptedException {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));
        for (int i = 0; i < 10; i++) {
            c.save(new BasicDBObject("x", i), WriteConcern.ACKNOWLEDGED);
        }

        final DBCursor cur = c.find()
                              .sort(new BasicDBObject("$natural", 1))
                              .addOption(Bytes.QUERYOPTION_TAILABLE);

        final CountDownLatch latch = new CountDownLatch(1);
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // the following call will block on the last hasNext
                int i = 0;
                while (i < 11) {
                    DBObject obj = cur.next();
                    i++;
                    if (i == 10) {
                        latch.countDown();
                    } else if (i > 10) {
                        return (Integer) obj.get("x");
                    }
                }
                return null;
            }
        };

        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<Integer> future = es.submit(callable);

        latch.await(5, SECONDS);

        // this doc should unblock thread
        c.save(new BasicDBObject("x", 10), WriteConcern.ACKNOWLEDGED);
        assertEquals(10, (long) future.get(5, SECONDS));
    }

    @Test
    public void shouldSupportTryNextOnTailableCursors() {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));

        c.save(new BasicDBObject("x", 1), WriteConcern.ACKNOWLEDGED);
        DBCursor cur = c.find()
                        .sort(new BasicDBObject("$natural", 1))
                        .addOption(Bytes.QUERYOPTION_TAILABLE);

        try {
            cur.tryNext();
        } catch (IllegalArgumentException e) {
            fail();
        }
    }

    @Test
    public void shouldSupportTryNextOnTailableAwaitCursors() {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));

        c.save(new BasicDBObject("x", 1), WriteConcern.ACKNOWLEDGED);
        DBCursor cur = c.find()
                        .sort(new BasicDBObject("$natural", 1))
                        .addOption(Bytes.QUERYOPTION_TAILABLE)
                        .addOption(Bytes.QUERYOPTION_AWAITDATA);

        try {
            cur.tryNext();
        } catch (IllegalArgumentException e) {
            fail();
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnTryNextForNonTailableCursors() {
        DBCollection c = database.getCollection("tail1");
        c.drop();
        database.createCollection("tail1", new BasicDBObject("capped", true).append("size", 10000));

        c.save(new BasicDBObject("x", 1), WriteConcern.ACKNOWLEDGED);
        DBCursor cur = c.find()
                        .sort(new BasicDBObject("$natural", 1))
                        .addOption(Bytes.QUERYOPTION_AWAITDATA);

        cur.tryNext();
    }

    @Test
    public void testExplain() {
        assumeThat(serverVersionAtLeast(asList(3, 0, 0)), is(false));
        insertTestData(collection, 100);

        DBObject q = BasicDBObjectBuilder.start().push("x").add("$gt", 50).get();

        assertEquals(49, collection.find(q).count());
        assertEquals(49, collection.find(q).itcount());
        assertEquals(49, collection.find(q).toArray().size());
        assertEquals(49, collection.find(q).itcount());
        assertEquals(20, collection.find(q).limit(20).itcount());
        assertEquals(20, collection.find(q).limit(-20).itcount());

        collection.createIndex(new BasicDBObject("x", 1));

        assertEquals(49, collection.find(q).count());
        assertEquals(49, collection.find(q).toArray().size());
        assertEquals(49, collection.find(q).itcount());
        assertEquals(20, collection.find(q).limit(20).itcount());
        assertEquals(20, collection.find(q).limit(-20).itcount());

        assertEquals(49, collection.find(q).explain().get("n"));

        assertEquals(20, collection.find(q).limit(20).explain().get("n"));
        assertEquals(20, collection.find(q).limit(-20).explain().get("n"));
    }

    @Test
    public void testBatchWithLimit() {
        insertTestData(collection, 100);

        assertEquals(50, collection.find().limit(50).itcount());
        assertEquals(50, collection.find().batchSize(5).limit(50).itcount());
    }

    @Test
    public void testSpecial() {
        insertTestData(collection, 3);
        collection.createIndex(new BasicDBObject("x", 1));

        for (final DBObject o : collection.find().sort(new BasicDBObject("x", 1)).addSpecial("$returnKey", 1)) {
            assertTrue(o.get("_id") == null);
        }

        for (final DBObject o : collection.find().sort(new BasicDBObject("x", 1))) {
            assertTrue(o.get("_id") != null);
        }

    }

    @Test
    public void testUpsert() {
        collection.update(new BasicDBObject("page", "/"), new BasicDBObject("$inc", new BasicDBObject("count", 1)), true, false);
        collection.update(new BasicDBObject("page", "/"), new BasicDBObject("$inc", new BasicDBObject("count", 1)), true, false);

        assertEquals(1, collection.getCount());
        assertEquals(2, collection.findOne().get("count"));
    }

    @Test
    public void testLimitAndBatchSize() {
        insertTestData(collection, 1000);

        DBObject q = BasicDBObjectBuilder.start().push("x").add("$lt", 200).get();

        DBCursor cur = collection.find(q);
        assertEquals(0, cur.getCursorId());
        assertEquals(200, cur.itcount());

        cur = collection.find(q).limit(50);
        assertEquals(0, cur.getCursorId());
        assertEquals(50, cur.itcount());

        cur = collection.find(q).batchSize(50);
        assertEquals(0, cur.getCursorId());
        assertEquals(200, cur.itcount());

        cur = collection.find(q).batchSize(100).limit(50);
        assertEquals(0, cur.getCursorId());
        assertEquals(50, cur.itcount());

        cur = collection.find(q).batchSize(-40);
        assertEquals(0, cur.getCursorId());
        assertEquals(40, cur.itcount());

        cur = collection.find(q).limit(-100);
        assertEquals(0, cur.getCursorId());
        assertEquals(100, cur.itcount());

        cur = collection.find(q).batchSize(-40).limit(20);
        assertEquals(0, cur.getCursorId());
        assertEquals(20, cur.itcount());

        cur = collection.find(q).batchSize(-20).limit(100);
        assertEquals(0, cur.getCursorId());
        assertEquals(20, cur.itcount());
    }

    @Test
    public void testCurrentObjectAndNumSeen() {
        for (int i = 1; i < 100; i++) {
            collection.save(new BasicDBObject("x", i));
        }

        DBCursor cur = collection.find().sort(new BasicDBObject("x", 1));
        while (cur.hasNext()) {
            DBObject current = cur.next();
            int num = (Integer) current.get("x");

            assertEquals(current, cur.curr());
            assertEquals(num, cur.numSeen());
        }
    }

    @Test
    public void testSort() {
        for (int i = 0; i < 1000; i++) {
            collection.save(new BasicDBObject("x", i).append("y", 1000 - i));
        }

        //x ascending
        DBCursor cur = collection.find().sort(new BasicDBObject("x", 1));
        int curmax = -100;
        while (cur.hasNext()) {
            int val = (Integer) cur.next().get("x");
            assertTrue(val > curmax);
            curmax = val;
        }

        //x desc
        cur = collection.find().sort(new BasicDBObject("x", -1));
        curmax = 9999;
        while (cur.hasNext()) {
            int val = (Integer) cur.next().get("x");
            assertTrue(val < curmax);
            curmax = val;
        }

        //query and sort
        cur = collection.find(QueryBuilder.start("x").greaterThanEquals(500).get()).sort(new BasicDBObject("y", 1));
        assertEquals(500, cur.count());
        curmax = -100;
        while (cur.hasNext()) {
            int val = (Integer) cur.next().get("y");
            assertTrue(val > curmax);
            curmax = val;
        }
    }

    @Test(expected = NoSuchElementException.class)
    public void testShouldThrowNoSuchElementException() {
        DBCursor cursor = collection.find();
        cursor.next();
    }

    private void insertTestData(final DBCollection dbCollection, final int numberOfDocuments) {
        List<DBObject> documents = new ArrayList<DBObject>();
        for (int i = 0; i < numberOfDocuments; i++) {
            documents.add(new BasicDBObject("x", i));
        }
        dbCollection.insert(documents);
    }

    //TODO: why is this commented out?
    //    @Test
    //    public void testHasFinalizer() throws UnknownHostException, UnknownHostException {
    //        DBCollection c = database.getCollection("HasFinalizerTest");
    //        c.drop();
    //
    //        for (int i = 0; i < 1000; i++) {
    //            c.save(new BasicDBObject("_id", i), WriteConcern.ACKNOWLEDGED);
    //        }
    //
    //        // finalizer is on by default so after calling hasNext should report that it has one
    //        DBCursor cursor = c.find();
    //        assertFalse(cursor.hasFinalizer());
    //        cursor.hasNext();
    //        assertTrue(cursor.hasFinalizer());
    //        cursor.close();
    //
    //        // no finalizer if there is no cursor, as there should not be for a query with only one result
    //        cursor = c.find(new BasicDBObject("_id", 1));
    //        cursor.hasNext();
    //        assertFalse(cursor.hasFinalizer());
    //        cursor.close();
    //
    //        // no finalizer if there is no cursor, as there should not be for a query with negative batch size
    //        cursor = c.find();
    //        cursor.batchSize(-1);
    //        cursor.hasNext();
    //        assertFalse(cursor.hasFinalizer());
    //        cursor.close();
    //
    //        // finally, no finalizer if disabled in mongo options
    //        MongoClientOptions mongoOptions = MongoClientOptions.builder().build();
    //        mongoOptions.cursorFinalizerEnabled = false;
    //        Mongo m = new MongoClient("127.0.0.1", mongoOptions);
    //        try {
    //            c = m.getDB(cleanupDB).getCollection("HasFinalizerTest");
    //            cursor = c.find();
    //            cursor.hasNext();
    //            assertFalse(cursor.hasFinalizer());
    //            cursor.close();
    //        } finally {
    //            m.close();
    //        }
    //    }
}
