package org.infinispan.query.persistence;

import static org.infinispan.test.TestingUtil.withCacheManager;
import static org.testng.Assert.assertEquals;

import java.io.File;
import java.io.Serializable;
import java.util.List;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
import org.infinispan.Cache;
import org.infinispan.commons.util.Util;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.Index;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.query.CacheQuery;
import org.infinispan.query.Search;
import org.infinispan.query.SearchManager;
import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.test.CacheManagerCallable;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

/**
 * Tests persistent index state us in synch with the values stored in a CacheLoader
 * after a CacheManager is restarted.
 *
 * @author Jan Slezak
 * @author Sanne Grinovero
 * @since 5.2
 */
@Test(groups = "functional", testName = "query.persistence.InconsistentIndexesAfterRestartTest")
public class InconsistentIndexesAfterRestartTest extends AbstractInfinispanTest {

    private static String TMP_DIR;

    @Test
    public void testPutSearchablePersistentWithoutBatchingWithoutTran() throws Exception {
        testPutTwice(false, false);
    }

    @Test
    public void testPutSearchablePersistentWithBatchingWithoutTran() throws Exception {
        testPutTwice(true, false);
    }

    @Test
    public void testPutSearchablePersistentWithBatchingInTran() throws Exception {
        testPutTwice(true, true);
    }

    private void testPutTwice(boolean batching, boolean inTran) throws Exception {
       testPutOperation(batching, inTran);
       testPutOperation(batching, inTran);
    }

    private void testPutOperation(boolean batching, final boolean inTran) throws Exception {
       withCacheManager(new CacheManagerCallable(getCacheManager(batching)) {
          @Override
          public void call() throws Exception {
             Cache<Object, Object> c = cm.getCache();
             if (inTran) c.getAdvancedCache().getTransactionManager().begin();
             c.put("key1", new SEntity(1, "name1", "surname1"));
             if (inTran) c.getAdvancedCache().getTransactionManager().commit();
             assertEquals(searchByName("name1", c).size(), 1, "should be 1, even repeating this");
          }
       });
    }

    private EmbeddedCacheManager getCacheManager(boolean batchingEnabled) throws Exception {
       ConfigurationBuilder builder = new ConfigurationBuilder();
       builder
          .persistence()
             .passivation(false)
          .addSingleFileStore()
             .location(TMP_DIR + File.separator + "cacheStore")
             .preload(false)
             .fetchPersistentState(true)
             .purgeOnStartup(false)
          .indexing()
             .index(Index.LOCAL)
             .addIndexedEntity(SEntity.class)
             .addProperty("default.directory_provider", "filesystem")
             .addProperty("lucene_version", "LUCENE_CURRENT")
             .addProperty("default.indexBase", TMP_DIR + File.separator + "idx");

       if (batchingEnabled) {
          builder.invocationBatching().enable();
       }
       else {
          builder.invocationBatching().disable();
       }

       return TestCacheManagerFactory.createCacheManager(builder);
    }

    private List searchByName(String name, Cache c) {
        SearchManager sm = Search.getSearchManager(c);
        CacheQuery<?> q = sm.getQuery(SEntity.searchByName(name), SEntity.class);
        int resultSize = q.getResultSize();
        List<?> l = q.list();
        assert l.size() == resultSize;
        return q.list();
    }

    @Indexed
    public static class SEntity implements Serializable {

        public static final String IDX_NAME = "name";

        public static final String IDX_SURNAME = "surname";

        private final long id;

        @Field(store = Store.YES)
        private final String name;

        @Field (store = Store.YES)
        private final String surname;

        public SEntity(long id, String name, String surname) {
            this.id = id;
            this.name = name;
            this.surname = surname;
        }

        public long getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public String getSurname() {
            return surname;
        }

        @Override
        public String toString() {
            return "SEntity{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", surname='" + surname + '\'' +
                    '}';
        }

       public static Query searchByName(String name) {
          return new BooleanQuery.Builder()
                  .add(new TermQuery(
                          new Term(SEntity.IDX_NAME, name.toLowerCase())), BooleanClause.Occur.MUST).build();
       }
    }

    @BeforeClass
    protected void setUpTempDir() {
       TMP_DIR = TestingUtil.tmpDirectory(this.getClass());
       new File(TMP_DIR).mkdirs();
    }

    @AfterClass
    protected void clearTempDir() {
       Util.recursiveFileRemove(TMP_DIR);
    }

}
