/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009 Red Hat Inc. and/or its affiliates and other
 * contributors as indicated by the @author tags. All rights reserved.
 * See the copyright.txt in the distribution for a full listing of
 * individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.infinispan.query.blackbox;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

import junit.framework.Assert;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.PrefixFilter;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.hibernate.search.FullTextFilter;
import org.hibernate.search.engine.spi.EntityIndexBinder;
import org.hibernate.search.engine.spi.SearchFactoryImplementor;
import org.hibernate.search.spi.SearchFactoryIntegrator;
import org.infinispan.Cache;
import org.infinispan.CacheImpl;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.factories.ComponentRegistry;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.query.CacheQuery;
import org.infinispan.query.FetchOptions;
import org.infinispan.query.ResultIterator;
import org.infinispan.query.Search;
import org.infinispan.query.SearchManager;
import org.infinispan.query.test.AnotherGrassEater;
import org.infinispan.query.test.CustomKey3;
import org.infinispan.query.test.CustomKey3Transformer;
import org.infinispan.query.test.Person;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.AssertJUnit;
import org.testng.annotations.Test;
import java.util.NoSuchElementException;

import static java.util.Arrays.asList;
import static org.infinispan.query.helper.TestQueryHelperFactory.createCacheQuery;
import static org.infinispan.query.helper.TestQueryHelperFactory.createQueryParser;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Test(groups = "functional", testName = "query.blackbox.LocalCacheTest")
public class LocalCacheTest extends SingleCacheManagerTest {
   protected Person person1;
   protected Person person2;
   protected Person person3;
   protected Person person4;
   protected AnotherGrassEater anotherGrassEater;
   protected QueryParser queryParser;
   protected String key1 = "Navin";
   protected String key2 = "BigGoat";
   protected String key3 = "MiniGoat";
   protected String anotherGrassEaterKey = "anotherGrassEaterKey";
   
   public LocalCacheTest() {
      cleanup = CleanupPhase.AFTER_METHOD;
   }

   public void testSimple() throws ParseException {
      loadTestingData();
      CacheQuery cacheQuery = createCacheQuery(cache, "blurb", "playing" );

      List<Object> found = cacheQuery.list();

      int elems = found.size();
      assert elems == 1 : "Expected 1 but was " + elems;

      Object val = found.get(0);
      assert val.equals(person1) : "Expected " + person1 + " but was " + val;
   }

   public void testEagerIterator() throws ParseException {
      loadTestingData();
      CacheQuery cacheQuery = createCacheQuery(cache, "blurb", "playing" );

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.EAGER));

      assert found.hasNext();
      found.next();
      assert !found.hasNext();
   }

   public void testMultipleResults() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2;
      assert found.get(0) == person2;
      assert found.get(1) == person3;

   }

   public void testModified() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      List<Object> found = cacheQuery.list();

      assert found.size() == 1;
      assert found.get(0).equals(person1);

      person1.setBlurb("Likes pizza");
      cache.put(key1, person1);

      queryParser = createQueryParser("blurb");
      luceneQuery = queryParser.parse("pizza");
      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      found = cacheQuery.list();

      assert found.size() == 1;
      assert found.get(0).equals(person1);
   }

   public void testAdded() throws ParseException {
      assertIndexingKnows(cache);
      loadTestingData();
      assertIndexingKnows(cache, Person.class, AnotherGrassEater.class);
      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("Goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2 : "Size of list should be 2";
      assert found.contains(person2);
      assert found.contains(person3);
      assert !found.contains(person4) : "This should not contain object person4";

      person4 = new Person();
      person4.setName("Mighty Goat");
      person4.setBlurb("Also eats grass");

      cache.put("mighty", person4);

      luceneQuery = queryParser.parse("Goat");
      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      found = cacheQuery.list();

      assert found.size() == 3 : "Size of list should be 3";
      assert found.contains(person2);
      assert found.contains(person3);
      assert found.contains(person4) : "This should now contain object person4";
   }

   public void testRemoved() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("Goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2;
      assert found.contains(person2);
      assert found.contains(person3) : "This should still contain object person3";

      cache.remove(key3);

      luceneQuery = queryParser.parse("Goat");
      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      found = cacheQuery.list();

      assert found.size() == 1;
      assert found.contains(person2);
      assert !found.contains(person3) : "The search should not return person3";
   }

   public void testUpdated() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("Goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2 : "Size of list should be 2";
      assert found.contains(person2) : "The search should have person2";

      cache.put(key2, person1);

      luceneQuery = queryParser.parse("Goat");
      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      found = cacheQuery.list();
      
      assert found.size() == 1 : "Size of list should be 1";
      assert !found.contains(person2) : "Person 2 should not be found now";
      assert !found.contains(person1) : "Person 1 should not be found because it does not meet the search criteria";
   }

   public void testSetSort() throws ParseException {
      loadTestingData();
      person2.setAge(35);
      person3.setAge(12);

      Sort sort = new Sort( new SortField("age", SortField.STRING));

      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("Goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2;

      cacheQuery.sort(sort);

      found = cacheQuery.list();

      assert found.size() == 2;
      assert found.get(0).equals(person2);
      assert found.get(1).equals(person3);
   }

   public void testSetFilter() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("name");

      Query luceneQuery = queryParser.parse("goat");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      List<Object> found = cacheQuery.list();

      assert found.size() == 2;

      Filter filter = new PrefixFilter(new Term("blurb", "cheese"));

      cacheQuery.filter(filter);

      found = cacheQuery.list();

      assert found.size() == 1;
   }

   public void testLazyIterator() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.LAZY));

      assert found.hasNext();
      found.next();
      assert !found.hasNext();
   }

   @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Unknown FetchMode null")
   public void testUnknownFetchModeIterator() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      ResultIterator found = cacheQuery.iterator(new FetchOptions(){
         public FetchOptions fetchMode(FetchMode fetchMode) {
            return null;
         }
      });

      assert found.hasNext();
      found.next();
      assert !found.hasNext();
   }

   public void testIteratorWithDefaultOptions() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      ResultIterator found = cacheQuery.iterator();

      assert found.hasNext();
      found.next();
      assert !found.hasNext();
   }

   public void testExplain() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      int matchCounter = 0;
      for(int i = 0; i < 4; i++) {
         Explanation found = cacheQuery.explain(i);

         if(found.isMatch())
            matchCounter++;
      }

      AssertJUnit.assertEquals(3, matchCounter);
   }

   public void testFullTextFilterOnOff() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");

      CacheQuery query = Search.getSearchManager(cache).getQuery(luceneQuery);
      FullTextFilter filter = query.enableFullTextFilter("personFilter");
      filter.setParameter("blurbText", "cheese");

      AssertJUnit.assertEquals(1, query.getResultSize());
      List result = query.list();

      Person person = (Person) result.get(0);
      AssertJUnit.assertEquals("Mini Goat", person.getName());
      AssertJUnit.assertEquals("Eats cheese", person.getBlurb());

      //Disabling the fullTextFilter.
      query.disableFullTextFilter("personFilter");
      AssertJUnit.assertEquals(3, query.getResultSize());
   }

   @Test(expectedExceptions = UnsupportedOperationException.class)
   public void testIteratorRemove() throws ParseException {
      loadTestingData();

      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");

      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);
      ResultIterator iterator = cacheQuery.iterator();
      if(iterator.hasNext()) {
         Object next = iterator.next();
         iterator.remove();
      }
   }

   public void testLazyIteratorWithOffset() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery).firstResult(1);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.LAZY));

      int count = 0;
      //noinspection UnusedDeclaration
      while(found.hasNext()) {
          count++;
          found.next();
      }

      assert (count == 2);
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testSearchManagerWithNullCache() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("fish");
      CacheQuery cacheQuery = Search.getSearchManager(null).getQuery(luceneQuery).firstResult(1);
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testLazyIteratorWithInvalidFetchSize() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery).firstResult(1);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.LAZY).fetchSize(0));
   }

   @Test(expectedExceptions = NoSuchElementException.class)
   public void testLazyIteratorWithNoElementsFound() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("fish");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery).firstResult(1);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.LAZY));

      found.next();
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testIteratorWithNullFetchMode() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery).firstResult(1);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(null));

      found.next();
   }

   public void testSearchKeyTransformer() throws ParseException {
      SearchManager manager = Search.getSearchManager(cache);
      manager.registerKeyTransformer(CustomKey3.class, CustomKey3Transformer.class);

      loadTestingDataWithCustomKey();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("Eats");

      CacheQuery cacheQuery = manager.getQuery(luceneQuery);


      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.LAZY));

      int counter = 0;
      while(found.hasNext()) {
         found.next();
         counter++;
      }

      AssertJUnit.assertEquals(3, counter);
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testSearchWithWrongCache() throws ParseException {
      Cache cache = mock(CacheImpl.class);
      when(cache.getAdvancedCache()).thenReturn(null);

      SearchManager manager = Search.getSearchManager(cache);
   }

   //Another test just for covering Search.java instantiation, although it is unnecessary. As well as covering the
   //valueOf() method of FetchMode, again just for adding coverage.
   public void testSearchManagerWithInstantiation() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");

      Search search = new Search();
      CacheQuery cacheQuery = search.getSearchManager(cache).getQuery(luceneQuery);

      ResultIterator found = cacheQuery.iterator(new FetchOptions().fetchMode(FetchOptions.FetchMode.valueOf("LAZY")));

      assert found.hasNext();
      found.next();
      assert !found.hasNext();
   }

   public void testGetResultSize() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("playing");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      assert cacheQuery.getResultSize() == 1;
   }

   public void testClear() {
      loadTestingData();

      // Create a term that will return me everyone called Navin.
      Term navin = new Term("name", "navin");

      // Create a term that I know will return me everything with name goat.
      Term goat = new Term ("name", "goat");

      Query[] queries = new Query[2];
      queries[0] = new TermQuery(goat);
      queries[1] = new TermQuery(navin);

      Query luceneQuery = queries[0].combine(queries);
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      // We know that we've got all 3 hits.
      assert cacheQuery.getResultSize() == 3 : "Expected 3, got " + cacheQuery.getResultSize();

      cache.clear();

      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      assert cacheQuery.getResultSize() == 0;
   }

   public void testTypeFiltering() throws ParseException {
      loadTestingData();
      queryParser = createQueryParser("blurb");
      Query luceneQuery = queryParser.parse("grass");
      CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

      List<Object> found = cacheQuery.list();

      assert found.size() == 2;
      assert found.containsAll(asList(person2, anotherGrassEater));

      queryParser = createQueryParser("blurb");
      luceneQuery = queryParser.parse("grass");
      cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery, AnotherGrassEater.class);

      found = cacheQuery.list();

      assert found.size() == 1;
      assert found.get(0).equals(anotherGrassEater);
   }
   
   protected EmbeddedCacheManager createCacheManager() throws Exception {
      ConfigurationBuilder cfg = getDefaultStandaloneCacheConfig(true);
      cfg
         .indexing()
            .enable()
            .indexLocalOnly(false)
            .addProperty("default.directory_provider", "ram")
            .addProperty("lucene_version", "LUCENE_CURRENT");
      enhanceConfig(cfg);
      return TestCacheManagerFactory.createCacheManager(cfg);
   }
   
   public void testEntityDiscovery() {
      assertIndexingKnows(cache);

      Person p = new Person();
      p.setName("Lucene developer");
      p.setAge(30);
      p.setBlurb("works best on weekends");
      cache.put(p.getName(), p);
      
      assertIndexingKnows(cache, Person.class);
   }

   /**
    * Verifies if the indexing interceptor is aware of a specific list of types.
    * @param cache the cache containing the indexes
    * @param types vararg listing the types the indexing engine should know
    */
   private void assertIndexingKnows(Cache<Object, Object> cache, Class... types) {
      ComponentRegistry cr = cache.getAdvancedCache().getComponentRegistry();
      SearchFactoryImplementor searchFactoryIntegrator = (SearchFactoryImplementor) cr.getComponent(SearchFactoryIntegrator.class);
      Assert.assertNotNull(searchFactoryIntegrator);
      Map<Class<?>, EntityIndexBinder> indexBindingForEntity = searchFactoryIntegrator.getIndexBindingForEntity();
      Assert.assertNotNull(indexBindingForEntity);
      Set<Class<?>> keySet = indexBindingForEntity.keySet();
      Assert.assertEquals(types.length, keySet.size());
      Assert.assertTrue(keySet.containsAll(Arrays.asList(types)));
   }

   protected void loadTestingData() {
      prepareTestingData();

      cache.put(key1, person1);

      // person2 is verified as number of matches in multiple tests,
      // so verify duplicate insertion doesn't affect result counts:
      cache.put(key2, person2);
      cache.put(key2, person2);
      cache.put(key2, person2);

      cache.put(key3, person3);
      cache.put(anotherGrassEaterKey, anotherGrassEater);
   }

   protected void loadTestingDataWithCustomKey() {
      prepareTestingData();

      CustomKey3 customeKey1 = new CustomKey3(key1);
      CustomKey3 customeKey2 = new CustomKey3(key2);
      CustomKey3 customeKey3 = new CustomKey3(key3);
      cache.put(customeKey1, person1);

      // person2 is verified as number of matches in multiple tests,
      // so verify duplicate insertion doesn't affect result counts:
      cache.put(customeKey2, person2);
      cache.put(customeKey2, person2);
      cache.put(customeKey2, person2);

      cache.put(customeKey3, person3);
      cache.put(anotherGrassEaterKey, anotherGrassEater);
   }

   protected void prepareTestingData() {
      person1 = new Person();
      person1.setName("Navin Surtani");
      person1.setBlurb("Likes playing WoW");

      person2 = new Person();
      person2.setName("Big Goat");
      person2.setBlurb("Eats grass");

      person3 = new Person();
      person3.setName("Mini Goat");
      person3.setBlurb("Eats cheese");

      anotherGrassEater = new AnotherGrassEater("Another grass-eater", "Eats grass");
   }
   
   protected void enhanceConfig(ConfigurationBuilder c) {
      // no op, meant to be overridden
   }

}
