package org.apache.rya.indexing.mongo;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


import static org.apache.rya.api.resolver.RdfToRyaConversions.convertStatement;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;

import org.apache.accumulo.core.client.TableExistsException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.rya.indexing.StatementConstraints;
import org.apache.rya.indexing.TemporalInstant;
import org.apache.rya.indexing.TemporalInstantRfc3339;
import org.apache.rya.indexing.TemporalInterval;
import org.apache.rya.indexing.accumulo.ConfigUtils;
import org.apache.rya.indexing.mongodb.temporal.MongoTemporalIndexer;
import org.apache.rya.mongodb.MongoDBRdfConfiguration;
import org.apache.rya.mongodb.MongoRyaITBase;
import org.bson.Document;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.junit.Test;

import com.mongodb.MongoException;
import com.mongodb.MongoSecurityException;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;

/**
 * JUnit tests for TemporalIndexer and it's implementation MongoTemporalIndexer
 *
 * If you enjoy this test, please read RyaTemporalIndexerTest and YagoKBTest, which contain
 * many example SPARQL queries and updates and attempts to test independently of Mongo:
 *
 *     extras/indexingSail/src/test/java/org.apache/rya/indexing/Mongo/RyaTemporalIndexerTest.java
 *     {@link org.apache.rya.indexing.Mongo.RyaTemporalIndexerTest}
 *     {@link org.apache.rya.indexing.Mongo.YagoKBTest.java}
 *
 * Remember, this class in instantiated fresh for each @test method.
 * so fields are reset, unless they are static.
 *
 * These are covered:
 *   Instance {before, equals, after} given Instance
 *   Instance {before, after, inside} given Interval
 *   Instance {hasBeginning, hasEnd} given Interval
 * And a few more.
 *
 */
public final class MongoTemporalIndexerIT extends MongoRyaITBase {

    private static final String URI_PROPERTY_EVENT_TIME = "Property:event:time";
    private static final String URI_PROPERTY_CIRCA = "Property:circa";
    private static final String URI_PROPERTY_AT_TIME = "Property:atTime";
    private static final StatementConstraints EMPTY_CONSTRAINTS = new StatementConstraints();

    // Assign this in setUpBeforeClass, store them in each test.
    // setup() deletes table before each test.
    static final Statement spo_B00_E01;
    static final Statement spo_B03_E20;
    static final Statement spo_B02_E29;
    static final Statement spo_B02_E30;
    static final Statement spo_B02_E40;
    static final Statement spo_B02_E31;
    static final Statement spo_B29_E30;
    static final Statement spo_B30_E32;

    // Instants:
    static final Statement spo_B02;
    static final int SERIES_OF_SECONDS = 41;
    static final Statement seriesSpo[] = new Statement[SERIES_OF_SECONDS];

    // These are shared for several tests. Only the seconds are different.
    // tvB03_E20 read as: interval Begins 3 seconds, ends at 20 seconds
    static final TemporalInterval tvB00_E01 = new TemporalInterval(makeInstant(00), makeInstant(01));
    static final TemporalInterval tvB29_E30= new TemporalInterval(makeInstant(29), makeInstant(30));
    static final TemporalInterval tvB30_E32= new TemporalInterval(makeInstant(30), makeInstant(32));
    static final TemporalInterval tvB03_E20 = new TemporalInterval(makeInstant(03), makeInstant(20));
    // 30 seconds, Begins earlier, ends later
    static final TemporalInterval tvB02_E30= new TemporalInterval(makeInstant(02), makeInstant(30));
    // use for interval after
    static final TemporalInterval tvB02_E29= new TemporalInterval(makeInstant(02), makeInstant(29));
    // same as above, but ends in the middle
    static final TemporalInterval tvB02_E31 = new TemporalInterval(makeInstant(02), makeInstant(31));
    // same as above, but ends even later
    static final TemporalInterval tvB02_E40 = new TemporalInterval(makeInstant(02), makeInstant(40));
    // instant, match beginnings of several above, before tiB03_E20
    static final TemporalInstant tsB02 = makeInstant(02);
    // instant, after all above
    static final TemporalInstant tsB04 = makeInstant(04);

    // Create a series of instants about times 0 - 40 seconds
    static final TemporalInstant seriesTs[];
    static {
        seriesTs = new TemporalInstant[SERIES_OF_SECONDS];
        for (int i = 0; i <= 40; i++) {
            seriesTs[i] = makeInstant(i);
        }
    };

    /**
     * Make an uniform instant with given seconds.
     */
    static TemporalInstant makeInstant(final int secondsMakeMeUnique) {
        return new TemporalInstantRfc3339(2015, 12, 30, 12, 00, secondsMakeMeUnique);
    }

    static {
        // Setup the statements only once. Each test will store some of these in there own index table.
        final ValueFactory vf = SimpleValueFactory.getInstance();
        final IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
        // tiB03_E20 read as: time interval that Begins 3 seconds, ends at 20 seconds,
        // Each time element the same, except seconds. year, month, .... minute are the same for each statement below.
        spo_B00_E01 = vf.createStatement(vf.createIRI("foo:event0"), pred1_atTime, vf.createLiteral(tvB00_E01.toString()));
        spo_B02_E29 = vf.createStatement(vf.createIRI("foo:event2"), pred1_atTime, vf.createLiteral(tvB02_E29.toString()));
        spo_B02_E30 = vf.createStatement(vf.createIRI("foo:event2"), pred1_atTime, vf.createLiteral(tvB02_E30.toString()));
        spo_B02_E31 = vf.createStatement(vf.createIRI("foo:event3"), pred1_atTime, vf.createLiteral(tvB02_E31.toString()));
        spo_B02_E40 = vf.createStatement(vf.createIRI("foo:event4"), pred1_atTime, vf.createLiteral(tvB02_E40.toString()));
        spo_B03_E20 = vf.createStatement(vf.createIRI("foo:event5"), pred1_atTime, vf.createLiteral(tvB03_E20.toString()));
        spo_B29_E30 = vf.createStatement(vf.createIRI("foo:event1"), pred1_atTime, vf.createLiteral(tvB29_E30.toString()));
        spo_B30_E32 = vf.createStatement(vf.createIRI("foo:event1"), pred1_atTime, vf.createLiteral(tvB30_E32.toString()));
        spo_B02 = vf.createStatement(vf.createIRI("foo:event6"), pred1_atTime, vf.createLiteral(tsB02.getAsReadable()));

        // Create statements about time instants 0 - 40 seconds
        for (int i = 0; i < seriesTs.length; i++) {
            seriesSpo[i] = vf.createStatement(vf.createIRI("foo:event0" + i), pred1_atTime, vf.createLiteral(seriesTs[i].getAsReadable()));
        }
    }

    @Override
    protected void updateConfiguration(final MongoDBRdfConfiguration conf) {
        // This is from http://linkedevents.org/ontology
        // and http://motools.sourceforge.net/event/event.html
        conf.setStrings(ConfigUtils.TEMPORAL_PREDICATES_LIST, ""
                + URI_PROPERTY_AT_TIME + ","
                + URI_PROPERTY_CIRCA + ","
                + URI_PROPERTY_EVENT_TIME);
    }

    /**
     * Test method for {@link MongoTemporalIndexer#storeStatement(convertStatement(org.eclipse.rdf4j.model.Statement)}
     */
    @Test
    public void testStoreStatement() throws IOException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            final ValueFactory vf = SimpleValueFactory.getInstance();

            final IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
            final IRI pred2_circa = vf.createIRI(URI_PROPERTY_CIRCA);

            // Should not be stored because they are not in the predicate list
            final String validDateStringWithThirteens = "1313-12-13T13:13:13Z";
            tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj1"), RDFS.LABEL, vf.createLiteral(validDateStringWithThirteens))));

            final String invalidDateString = "ThisIsAnInvalidDate";
            tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj2"), pred1_atTime, vf.createLiteral(invalidDateString))));

            // These are different datetimes instant but from different time zones.
            // This is an arbitrary zone, BRST=Brazil, better if not local.
            // same as "2015-01-01T01:59:59Z"
            final String testDate2014InBRST = "2014-12-31T23:59:59-02:00";
            // next year, same as "2017-01-01T01:59:59Z"
            final String testDate2016InET = "2016-12-31T20:59:59-05:00";

            // These should be stored because they are in the predicate list.
            // BUT they will get converted to the same exact datetime in UTC.
            final Statement s3 = vf.createStatement(vf.createIRI("foo:subj3"), pred1_atTime, vf.createLiteral(testDate2014InBRST));
            final Statement s4 = vf.createStatement(vf.createIRI("foo:subj4"), pred2_circa, vf.createLiteral(testDate2016InET));
            tIndexer.storeStatement(convertStatement(s3));
            tIndexer.storeStatement(convertStatement(s4));

            // This should not be stored because the object is not a literal
            tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj5"), pred1_atTime, vf.createIRI("in:valid"))));

            printTables(tIndexer, "junit testing: Temporal entities stored in testStoreStatement");
            assertEquals(2, tIndexer.getCollection().countDocuments());
        }
    }

    @Test
    public void testDelete() throws IOException, MongoException, TableNotFoundException, TableExistsException, NoSuchAlgorithmException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            final ValueFactory vf = SimpleValueFactory.getInstance();

            final IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
            final IRI pred2_circa = vf.createIRI(URI_PROPERTY_CIRCA);

            final String testDate2014InBRST = "2014-12-31T23:59:59-02:00";
            final String testDate2016InET = "2016-12-31T20:59:59-05:00";

            // These should be stored because they are in the predicate list.
            // BUT they will get converted to the same exact datetime in UTC.
            final Statement s1 = vf.createStatement(vf.createIRI("foo:subj3"), pred1_atTime, vf.createLiteral(testDate2014InBRST));
            final Statement s2 = vf.createStatement(vf.createIRI("foo:subj4"), pred2_circa, vf.createLiteral(testDate2016InET));
            tIndexer.storeStatement(convertStatement(s1));
            tIndexer.storeStatement(convertStatement(s2));

            final String dbName = conf.getMongoDBName();
            final MongoDatabase db = super.getMongoClient().getDatabase(dbName);
            final MongoCollection<Document> collection = db.getCollection(conf.get(MongoDBRdfConfiguration.MONGO_COLLECTION_PREFIX, "rya") + tIndexer.getCollectionName());

            printTables(tIndexer, "junit testing: Temporal entities stored in testDelete before delete");
            assertEquals("Number of rows stored.", 2, collection.countDocuments()); // 4 index entries per statement

            tIndexer.deleteStatement(convertStatement(s1));
            tIndexer.deleteStatement(convertStatement(s2));

            printTables(tIndexer, "junit testing: Temporal entities stored in testDelete after delete");
            assertEquals("Number of rows stored after delete.", 0, collection.countDocuments());
        }
    }

    /**
     * Test instant after a given instant.
     * From the series: instant {equal, before, after} instant
     * @throws MongoSecurityException
     * @throws MongoException
     * @throws TableNotFoundException
     */
    @Test
    public void testQueryInstantAfterInstant() throws IOException, QueryEvaluationException, TableNotFoundException, MongoException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
            final int searchForSeconds = 4;
            final int expectedResultCount = 9;
            for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantAfterInstant(seriesTs[searchForSeconds], EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[searchForSeconds + count + 1]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }
    /**
     * Test instant before a given instant.
     * From the series: instant {equal, before, after} instant
     */
    @Test
    public void testQueryInstantBeforeInstant() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
            final int searchForSeconds = 4;
            final int expectedResultCount = 4;
            for (int s = 0; s <= searchForSeconds + 15; s++) { // <== logic here
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;

            iter = tIndexer.queryInstantBeforeInstant(seriesTs[searchForSeconds], EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[count]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test instant before given interval.
     * From the series:  Instance {before, after, inside} given Interval
     */
    @Test
    public void testQueryInstantBeforeInterval() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
            final TemporalInterval searchForSeconds = tvB02_E31;
            final int expectedResultCount = 2; // 00 and 01 seconds.
            for (int s = 0; s <= 40; s++) { // <== logic here
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantBeforeInterval(searchForSeconds, EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[count]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test instant after given interval.
     * Instance {before, after, inside} given Interval
     */
    @Test
    public void testQueryInstantAfterInterval() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
            final TemporalInterval searchAfterInterval = tvB02_E31; // from 2 to 31 seconds
            final int endingSeconds = 31;
            final int expectedResultCount = 9; // 32,33,...,40 seconds.
            for (int s = 0; s <= endingSeconds + expectedResultCount; s++) { // <== logic here
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantAfterInterval(searchAfterInterval, EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[count + endingSeconds + 1]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test instant inside given interval.
     * Instance {before, after, inside} given Interval
     */
    @Test
    public void testQueryInstantInsideInterval() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
            final TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
            final int beginningSeconds = 2; // <== logic here, and next few lines.
            final int endingSeconds = 31;
            final int expectedResultCount = endingSeconds - beginningSeconds - 1; // 3,4,...,30 seconds.
            for (int s = 0; s <= 40; s++) {
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantInsideInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[count + beginningSeconds + 1]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test instant is the Beginning of the given interval.
     * from the series: Instance {hasBeginning, hasEnd} Interval
     */
    @Test
    public void testQueryInstantHasBeginningInterval() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
            final TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
            final int searchSeconds = 2; // <== logic here, and next few lines.
            final int expectedResultCount = 1; // 2 seconds.
            for (int s = 0; s <= 10; s++) {
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantHasBeginningInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[searchSeconds]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test instant is the end of the given interval.
     * from the series: Instance {hasBeginning, hasEnd} Interval
     */
    @Test
    public void testQueryInstantHasEndInterval()  throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
            final TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
            final int searchSeconds = 31; // <== logic here, and next few lines.
            final int expectedResultCount = 1; // 31 seconds.
            for (int s = 0; s <= 40; s++) {
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryInstantHasEndInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
            int count = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[searchSeconds]; // <== logic here
                assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                count++;
            }
            assertEquals("Should find count of rows.", expectedResultCount, count);
        }
    }

    /**
     * Test method for
     * {@link org.apache.rya.indexing.Mongo.temporal.MongoTemporalIndexer#queryIntervalEquals(TemporalInterval, StatementConstraints)}
     * .
     * @throws IOException
     * @throws QueryEvaluationException
     *
     */
    @Test
    public void testQueryIntervalEquals() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));
            tIndexer.storeStatement(convertStatement(seriesSpo[4])); // instance at 4 seconds


            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryIntervalEquals(tvB02_E40, EMPTY_CONSTRAINTS);
            // Should be found twice:
            assertTrue("queryIntervalEquals: spo_B02_E40 should be found, but actually returned empty results. spo_B02_E40=" + spo_B02_E40, iter.hasNext());
            assertTrue("queryIntervalEquals: spo_B02_E40 should be found, but does not match.", spo_B02_E40.equals(iter.next()));
            assertFalse("queryIntervalEquals: Find no more than one, but actually has more.", iter.hasNext());
        }
    }

    /**
     * Test interval before a given interval, for method:
     * {@link MongoTemporalIndexer#queryIntervalBefore(TemporalInterval, StatementConstraints)}.
     *
     * @throws IOException
     * @throws QueryEvaluationException
     */
    @Test
    public void testQueryIntervalBefore() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            tIndexer.storeStatement(convertStatement(spo_B00_E01));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            // instants should be ignored.
            tIndexer.storeStatement(convertStatement(spo_B30_E32));
            tIndexer.storeStatement(convertStatement(seriesSpo[1])); // instance at 1 seconds
            tIndexer.storeStatement(convertStatement(seriesSpo[2]));
            tIndexer.storeStatement(convertStatement(seriesSpo[31]));


            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryIntervalBefore(tvB02_E31, EMPTY_CONSTRAINTS);
            // Should be found twice:
            assertTrue("spo_B00_E01 should be found, but actually returned empty results. spo_B00_E01=" + spo_B00_E01, iter.hasNext());
            assertTrue("spo_B00_E01 should be found, but found another.", spo_B00_E01.equals(iter.next()));
            assertFalse("Find no more than one, but actually has more.", iter.hasNext());
        }
    }

    /**
     * interval is after the given interval.  Find interval beginnings after the endings of the given interval.
     * {@link MongoTemporalIndexer#queryIntervalAfter(TemporalInterval, StatementContraints).
     *
     * @throws IOException
     * @throws QueryEvaluationException
     */
    @Test
    public void testQueryIntervalAfter() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            tIndexer.storeStatement(convertStatement(spo_B00_E01));
            tIndexer.storeStatement(convertStatement(spo_B02_E29)); //<- after this one.
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B29_E30));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));
            // instants should be ignored.
            tIndexer.storeStatement(convertStatement(spo_B02));
            tIndexer.storeStatement(convertStatement(seriesSpo[1])); // instance at 1 seconds
            tIndexer.storeStatement(convertStatement(seriesSpo[2]));
            tIndexer.storeStatement(convertStatement(seriesSpo[31]));

            CloseableIteration<Statement, QueryEvaluationException> iter;
            iter = tIndexer.queryIntervalAfter(tvB02_E29, EMPTY_CONSTRAINTS);
            // Should be found twice:
            assertTrue("spo_B30_E32 should be found, but actually returned empty results. spo_B30_E32=" + spo_B30_E32, iter.hasNext());
            final Statement s = iter.next();
            assertTrue("spo_B30_E32 should be found, but found another. spo_B30_E32="+spo_B30_E32+", but found="+s, spo_B30_E32.equals(s));
            assertFalse("Find no more than one, but actually has more.", iter.hasNext());
        }
    }

    /**
     * Test instant after a given instant WITH two different predicates as constraints.
     */
    @Test
    public void testQueryWithMultiplePredicates() throws IOException, QueryEvaluationException {
        try(MongoTemporalIndexer tIndexer = new MongoTemporalIndexer()) {
            tIndexer.setConf(conf);
            tIndexer.init();

            // tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
            // these should not match as they are not instances.
            tIndexer.storeStatement(convertStatement(spo_B03_E20));
            tIndexer.storeStatement(convertStatement(spo_B02_E30));
            tIndexer.storeStatement(convertStatement(spo_B02_E40));
            tIndexer.storeStatement(convertStatement(spo_B02_E31));
            tIndexer.storeStatement(convertStatement(spo_B30_E32));

            // seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
            final int searchForSeconds = 4;
            final int expectedResultCount = 9;
            for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
                tIndexer.storeStatement(convertStatement(seriesSpo[s]));
            }
            final ValueFactory vf = SimpleValueFactory.getInstance();
            final IRI pred3_CIRCA_ = vf.createIRI(URI_PROPERTY_CIRCA);  // this one to ignore.
            final IRI pred2_eventTime = vf.createIRI(URI_PROPERTY_EVENT_TIME);
            final IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);

            // add the predicate = EventTime ; Store in an array for verification.
            final Statement[] SeriesTs_EventTime = new Statement[expectedResultCount+1];
            for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
                final Statement statement = vf.createStatement(vf.createIRI("foo:EventTimeSubj0" + s), pred2_eventTime, vf.createLiteral(seriesTs[s].getAsReadable()));
                tIndexer.storeStatement(convertStatement(statement));
                if (s>searchForSeconds) {
                    SeriesTs_EventTime[s - searchForSeconds -1 ] = statement;
                }
            }
            // add the predicate = CIRCA ; to be ignored because it is not in the constraints.
            for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
                final Statement statement = vf.createStatement(vf.createIRI("foo:CircaEventSubj0" + s), pred3_CIRCA_, vf.createLiteral(seriesTs[s].getAsReadable()));
                tIndexer.storeStatement(convertStatement(statement));
            }

            CloseableIteration<Statement, QueryEvaluationException> iter;
            final StatementConstraints constraints = new StatementConstraints();
            constraints.setPredicates(new HashSet<>(Arrays.asList( pred2_eventTime,  pred1_atTime )));

            iter = tIndexer.queryInstantAfterInstant(seriesTs[searchForSeconds], constraints); // EMPTY_CONSTRAINTS);//
            int count_AtTime = 0;
            int count_EventTime = 0;
            while (iter.hasNext()) {
                final Statement s = iter.next();
                final Statement nextExpectedStatement = seriesSpo[searchForSeconds + count_AtTime + 1]; // <== logic here
                if (s.getPredicate().equals(pred1_atTime)) {
                    assertTrue("Should match atTime: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
                    count_AtTime++;
                }
                else if (s.getPredicate().equals(pred2_eventTime)) {
                    assertTrue("Should match eventTime: " + SeriesTs_EventTime[count_EventTime] + " == " + s, SeriesTs_EventTime[count_EventTime].equals(s));
                    count_EventTime++;
                } else {
                    assertTrue("This predicate should not be returned: "+s, false);
                }

            }

            assertEquals("Should find count of atTime    rows.", expectedResultCount, count_AtTime);
            assertEquals("Should find count of eventTime rows.", expectedResultCount, count_EventTime);
        }
    }

    /**
     * Print and gather statistics on the entire index table.
     *
     * @param description
     *            Printed to the console to find the test case.
     * @param out
     *            null or System.out or other output to send a listing.
     * @param statistics
     *            Hashes, sums, and counts for assertions.
     * @return Count of entries in the index table.
     * @throws IOException
     */
    public void printTables(final MongoTemporalIndexer tIndexer, final String description) throws IOException {
        System.out.println("-- start printTables() -- " + description);
        System.out.println("Reading : " + tIndexer.getCollection().getNamespace().getFullName());
        try (final MongoCursor<Document> cursor = tIndexer.getCollection().find().iterator()) {
            while(cursor.hasNext()) {
                final Document doc = cursor.next();
                System.out.println(doc.toString());
            }
        }
        System.out.println();
    }
}
