/*
 * 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 com.mongodb.QueryBuilder.QueryBuilderException;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Pattern;

import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class QueryBuilderTest extends DatabaseTestCase {

    @Test
    public void elemMatchTest() {
        DBObject query = QueryBuilder.start("array").elemMatch(
                                                                  QueryBuilder.start("x").is(1).and("y").is(2).get()).get();
        DBObject expected = new BasicDBObject("array", new BasicDBObject("$elemMatch",
                                                                         new BasicDBObject("x", 1).append("y", 2)));
        assertEquals(expected, query);
        // TODO: add integration test
    }

    @Test
    public void notTest() {
        Pattern pattern = Pattern.compile("\\w*");
        DBObject query = QueryBuilder.start("x").not().regex(pattern).get();
        DBObject expected = new BasicDBObject("x", new BasicDBObject("$not", pattern));
        assertEquals(expected, query);

        query = QueryBuilder.start("x").not().regex(pattern).and("y").is("foo").get();
        expected = new BasicDBObject("x", new BasicDBObject("$not", pattern)).append("y", "foo");
        assertEquals(expected, query);

        query = QueryBuilder.start("x").not().greaterThan(2).get();
        expected = new BasicDBObject("x", new BasicDBObject("$not", new BasicDBObject("$gt", 2)));
        assertEquals(expected, query);

        query = QueryBuilder.start("x").not().greaterThan(2).and("y").is("foo").get();
        expected = new BasicDBObject("x", new BasicDBObject("$not", new BasicDBObject("$gt", 2))).append("y", "foo");
        assertEquals(expected, query);


        query = QueryBuilder.start("x").not().greaterThan(2).lessThan(0).get();
        expected = new BasicDBObject("x", new BasicDBObject("$not", new BasicDBObject("$gt", 2).append("$lt", 0)));
        assertEquals(expected, query);

    }

    @Test
    public void greaterThanTest() {
        String key = "x";
        saveTestDocument(collection, key, 0);

        DBObject queryTrue = QueryBuilder.start(key).greaterThan(-1).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).greaterThan(0).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void greaterThanEqualsTest() {
        String key = "x";
        saveTestDocument(collection, key, 0);

        DBObject queryTrue = QueryBuilder.start(key).greaterThanEquals(0).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryTrue2 = QueryBuilder.start(key).greaterThanEquals(-1).get();
        assertTrue(testQuery(collection, queryTrue2));

        DBObject queryFalse = QueryBuilder.start(key).greaterThanEquals(1).get();
        assertFalse(testQuery(collection, queryFalse));

    }

    @Test
    public void lessThanTest() {
        String key = "x";
        saveTestDocument(collection, key, 0);

        DBObject queryTrue = QueryBuilder.start(key).lessThan(1).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).lessThan(0).get();
        assertFalse(testQuery(collection, queryFalse));

    }

    @Test
    public void lessThanEqualsTest() {
        String key = "x";
        saveTestDocument(collection, key, 0);

        DBObject queryTrue = QueryBuilder.start(key).lessThanEquals(1).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryTrue2 = QueryBuilder.start(key).lessThanEquals(0).get();
        assertTrue(testQuery(collection, queryTrue2));

        DBObject queryFalse = QueryBuilder.start(key).lessThanEquals(-1).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void isTest() {
        String key = "x";
        saveTestDocument(collection, key, "test");

        DBObject queryTrue = QueryBuilder.start(key).is("test").get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).is("test1").get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void notEqualsTest() {
        String key = "x";
        saveTestDocument(collection, key, "test");

        DBObject queryTrue = QueryBuilder.start(key).notEquals("test1").get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).notEquals("test").get();
        assertFalse(testQuery(collection, queryFalse));

    }

    @Test
    public void inTest() {
        String key = "x";
        saveTestDocument(collection, key, 1);

        DBObject queryTrue = QueryBuilder.start(key).in(Arrays.asList(1, 2, 3)).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).in(Arrays.asList(2, 3, 4)).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void notInTest() {
        String key = "x";
        saveTestDocument(collection, key, 1);

        DBObject queryTrue = QueryBuilder.start(key).notIn(Arrays.asList(2, 3, 4)).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).notIn(Arrays.asList(1, 2, 3)).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void modTest() {
        String key = "x";
        saveTestDocument(collection, key, 9);

        DBObject queryTrue = QueryBuilder.start(key).mod(Arrays.asList(2, 1)).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).mod(Arrays.asList(2, 0)).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void allTest() {
        String key = "x";
        saveTestDocument(collection, key, Arrays.asList(1, 2, 3));

        DBObject query = QueryBuilder.start(key).all(Arrays.asList(1, 2, 3)).get();
        assertTrue(testQuery(collection, query));

        DBObject queryFalse = QueryBuilder.start(key).all(Arrays.asList(2, 3, 4)).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void sizeTest() {
        String key = "x";
        saveTestDocument(collection, key, Arrays.asList(1, 2, 3));

        DBObject queryTrue = QueryBuilder.start(key).size(3).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).size(4).get();
        assertFalse(testQuery(collection, queryFalse));

        DBObject queryFalse2 = QueryBuilder.start(key).size(2).get();
        assertFalse(testQuery(collection, queryFalse2));
    }

    @Test
    public void existsTest() {
        String key = "x";
        saveTestDocument(collection, key, "test");

        DBObject queryTrue = QueryBuilder.start(key).exists(true).get();
        assertTrue(testQuery(collection, queryTrue));

        DBObject queryFalse = QueryBuilder.start(key).exists(false).get();
        assertFalse(testQuery(collection, queryFalse));
    }

    @Test
    public void regexTest() {
        String key = "x";
        saveTestDocument(collection, key, "test");

        DBObject queryTrue = QueryBuilder.start(key).regex(Pattern.compile("\\w*")).get();
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test
    public void rangeChainTest() {
        String key = "x";
        saveTestDocument(collection, key, 2);

        DBObject queryTrue = QueryBuilder.start(key).greaterThan(0).lessThan(3).get();
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test
    public void compoundChainTest() {
        String key = "x";
        String key2 = "y";
        String value = key;
        DBObject testDocument = new BasicDBObject();
        testDocument.put(key, value);
        testDocument.put(key2, 9);
        collection.save(testDocument);

        DBObject queryTrue = QueryBuilder.start(key).is(value).and(key2).mod(Arrays.asList(2, 1)).get();
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test
    public void arrayChainTest() {
        String key = "x";
        saveTestDocument(collection, key, Arrays.asList(1, 2, 3));

        DBObject queryTrue = QueryBuilder.start(key).all(Arrays.asList(1, 2, 3)).size(3).get();
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test
    public void nearTest() {
        String key = "loc";
        BasicDBObject geoSpatialIndex = new BasicDBObject();
        geoSpatialIndex.put(key, "2d");
        collection.createIndex(geoSpatialIndex);

        Double[] coordinates = {(double) 50, (double) 30};
        saveTestDocument(collection, key, coordinates);

        DBObject queryTrue = QueryBuilder.start(key).near(45, 45).get();
        DBObject expected = new BasicDBObject(key, new BasicDBObject("$near", Arrays.asList(45.0, 45.0)));
        assertEquals(queryTrue, expected);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start(key).near(45, 45, 16).get();
        expected = new BasicDBObject(key, new BasicDBObject("$near", Arrays.asList(45.0, 45.0))
                .append("$maxDistance", 16.0));
        assertEquals(queryTrue, expected);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start(key).nearSphere(45, 45).get();
        expected = new BasicDBObject(key, new BasicDBObject("$nearSphere", Arrays.asList(45.0, 45.0)));
        assertEquals(queryTrue, expected);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start(key).nearSphere(45, 45, 0.5).get();
        expected = new BasicDBObject(key, new BasicDBObject("$nearSphere", Arrays.asList(45.0, 45.0))
                .append("$maxDistance", 0.5));
        assertEquals(queryTrue, expected);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start(key).withinCenterSphere(50, 30, 0.5).get();
        assertTrue(testQuery(collection, queryTrue));

        ArrayList<Double[]> points = new ArrayList<Double[]>();
        points.add(new Double[]{(double) 30, (double) 30});
        points.add(new Double[]{(double) 70, (double) 30});
        points.add(new Double[]{(double) 70, (double) 30});
        queryTrue = QueryBuilder.start(key).withinPolygon(points).get();
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionNoPolygonGivenForWithinQuery() {
        QueryBuilder.start("loc").withinPolygon(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionIfNoVerticesDefinedForPolygon() {
        QueryBuilder.start("loc").withinPolygon(new ArrayList<Double[]>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionIfInsufficientVerticesDefinedForPolygon() {
        QueryBuilder.start("loc").withinPolygon(Arrays.<Double[]>asList(new Double[]{30.0, 30.0}));
    }

    @Test
    public void textTest() {
        if (!serverVersionAtLeast(asList(2, 6, 0))) {
            return;
        }

        BasicDBObject enableTextCommand = new BasicDBObject("setParameter", 1).append("textSearchEnabled", true);
        database.getSisterDB("admin").command(enableTextCommand);
        DBCollection collection = database.getCollection("text-test");
        BasicDBObject textIndex = new BasicDBObject("comments", "text");
        collection.createIndex(textIndex);

        BasicDBObject doc = new BasicDBObject("comments", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
                .append("meaning", 42);
        collection.save(doc);

        DBObject queryTrue = QueryBuilder.start().text("dolor").get();
        DBObject expected = new BasicDBObject("$text", new BasicDBObject("$search", "dolor"));
        assertEquals(expected, queryTrue);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start().text("dolor", "english").get();
        expected = new BasicDBObject("$text", new BasicDBObject("$search", "dolor").append("$language", "english"));
        assertEquals(expected, queryTrue);
        assertTrue(testQuery(collection, queryTrue));

        queryTrue = QueryBuilder.start().and(
                QueryBuilder.start().text("dolor").get(),
                QueryBuilder.start("meaning").greaterThan(21).get()).get();
        expected = new BasicDBObject("$and",
                Arrays.asList(
                        new BasicDBObject("$text", new BasicDBObject("$search", "dolor")),
                        new BasicDBObject("meaning", new BasicDBObject("$gt", 21))));
        assertEquals(expected, queryTrue);
        assertTrue(testQuery(collection, queryTrue));
    }

    @Test(expected = QueryBuilderException.class)
    public void shouldThrowAnExceptionIfTetIsNotAtTheTopLevelOfTheQuery() {
        QueryBuilder.start("x").text("funny");
    }

    @Test
    public void failureTest() {
        boolean thrown = false;
        try {
            QueryBuilder.start("x").get();
        } catch (QueryBuilderException e) {
            thrown = true;
        }
        assertTrue(thrown);

        boolean thrown2 = false;
        try {
            QueryBuilder.start("x").exists(true).and("y").get();
        } catch (QueryBuilderException e) {
            thrown2 = true;
        }
        assertTrue(thrown2);

        boolean thrown3 = false;
        try {
            QueryBuilder.start("x").and("y").get();
        } catch (QueryBuilderException e) {
            thrown3 = true;
        }
        assertTrue(thrown3);
    }

    @Test
    public void testOr() {
        collection.drop();
        collection.insert(new BasicDBObject("a", 1));
        collection.insert(new BasicDBObject("b", 1));

        DBObject q = QueryBuilder.start().or(new BasicDBObject("a", 1), new BasicDBObject("b", 1)).get();

        assertEquals(2, collection.find(q).count());
    }

    @Test
    public void testAnd() {
        collection.drop();
        collection.insert(new BasicDBObject("a", 1).append("b", 1));
        collection.insert(new BasicDBObject("b", 1));

        DBObject q = QueryBuilder.start().and(new BasicDBObject("a", 1), new BasicDBObject("b", 1)).get();

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

    @Test
    public void testMultipleAnd() {
        collection.drop();
        collection.insert(new BasicDBObject("a", 1).append("b", 1));
        collection.insert(new BasicDBObject("b", 1));

        DBObject q = QueryBuilder.start().and(new BasicDBObject("a", 1), new BasicDBObject("b", 1)).get();

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

    /**
     * Convenience method that creates a new MongoDB Document with a key-value pair and saves it inside the specified collection
     *
     * @param collection Collection to save the new document to
     * @param key        key of the field to be inserted to the new document
     * @param value      value of the field to be inserted to the new document
     */
    private void saveTestDocument(final DBCollection collection, final String key, final Object value) {
        DBObject testDocument = new BasicDBObject();
        testDocument.put(key, value);
        collection.save(testDocument);
    }

    private boolean testQuery(final DBCollection collection, final DBObject query) {
        DBCursor cursor = collection.find(query);
        return cursor.hasNext();
    }
}

