/*
 * 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.
 */
package org.apache.solr.analytics.function.field;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.util.Bits;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.analytics.ExpressionFactory;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.util.RefCounted;
import org.junit.AfterClass;
import org.junit.BeforeClass;

public class AbstractAnalyticsFieldTest extends SolrTestCaseJ4 {

  private static IndexSchema indexSchema;
  private static SolrIndexSearcher searcher;
  private static RefCounted<SolrIndexSearcher> ref;

  protected static Map<String,Integer> singleInts; 
  protected static Map<String,Map<Integer,Integer>> multiInts;
  protected static Map<String,Long> singleLongs; 
  protected static Map<String,Map<Long,Integer>> multiLongs;
  protected static Map<String,Float> singleFloats; 
  protected static Map<String,Map<Float,Integer>> multiFloats;
  protected static Map<String,Double> singleDoubles; 
  protected static Map<String,Map<Double,Integer>> multiDoubles;
  protected static Map<String,Long> singleDates; 
  protected static Map<String,Map<Long,Integer>> multiDates;
  protected static Map<String,String> singleStrings; 
  protected static Map<String,Map<String,Integer>> multiStrings;
  protected static Map<String,Boolean> singleBooleans; 
  protected static Map<String,Map<Boolean,Integer>> multiBooleans;
  
  private static List<String> missingDocuments;
  
  @BeforeClass
  public static void createSchemaAndFields() throws Exception {
    initCore("solrconfig-analytics.xml","schema-analytics.xml");

    singleInts = new HashMap<>(); 
    multiInts = new HashMap<>();
    singleLongs = new HashMap<>(); 
    multiLongs = new HashMap<>();
    singleFloats = new HashMap<>(); 
    multiFloats = new HashMap<>();
    singleDoubles = new HashMap<>(); 
    multiDoubles = new HashMap<>();
    singleDates = new HashMap<>(); 
    multiDates = new HashMap<>();
    singleStrings = new HashMap<>(); 
    multiStrings = new HashMap<>();
    singleBooleans = new HashMap<>(); 
    multiBooleans = new HashMap<>();
    
    missingDocuments = new ArrayList<>();
    
    assertU(adoc("id", "-2"));
    missingDocuments.add("-2");
    assertU(adoc("id", "5"));
    missingDocuments.add("5");
    for (int i = -1; i < 5; ++i) {
      assertU(adoc(
          "id", "" + i, 
          
          "int_i_t", "" + i,
          "int_im_t", "" + i,
          "int_im_t", "" + (i + 10),
          "int_im_t", "" + (i + 10),
          "int_im_t", "" + (i + 20),
          
          "int_i_p", "" + i,
          "int_im_p", "" + i,
          "int_im_p", "" + (i + 10),
          "int_im_p", "" + (i + 10),
          "int_im_p", "" + (i + 20),
          
          "long_l_t", "" + i,
          "long_lm_t", "" + i,
          "long_lm_t", "" + (i + 10),
          "long_lm_t", "" + (i + 10),
          "long_lm_t", "" + (i + 20),
          
          "long_l_p", "" + i,
          "long_lm_p", "" + i,
          "long_lm_p", "" + (i + 10),
          "long_lm_p", "" + (i + 10),
          "long_lm_p", "" + (i + 20),
          
          "float_f_t", "" + (i + .75F),
          "float_fm_t", "" + (i + .75F),
          "float_fm_t", "" + (i + 10.75F),
          "float_fm_t", "" + (i + 10.75F),
          "float_fm_t", "" + (i + 20.75F),
          
          "float_f_p", "" + (i + .75F),
          "float_fm_p", "" + (i + .75F),
          "float_fm_p", "" + (i + 10.75F),
          "float_fm_p", "" + (i + 10.75F),
          "float_fm_p", "" + (i + 20.75F),
          
          "double_d_t", "" + (i + .5),
          "double_dm_t", "" + (i + .5),
          "double_dm_t", "" + (i + 10.5),
          "double_dm_t", "" + (i + 10.5),
          "double_dm_t", "" + (i + 20.5),
          
          "double_d_p", "" + (i + .5),
          "double_dm_p", "" + (i + .5),
          "double_dm_p", "" + (i + 10.5),
          "double_dm_p", "" + (i + 10.5),
          "double_dm_p", "" + (i + 20.5),
          
          "date_dt_t", (1800 + i) + "-12-31T23:59:59Z",
          "date_dtm_t", (1800 + i) + "-12-31T23:59:59Z",
          "date_dtm_t", (1800 + i + 10) + "-12-31T23:59:59Z",
          "date_dtm_t", (1800 + i + 10) + "-12-31T23:59:59Z",
          "date_dtm_t", (1800 + i + 20) + "-12-31T23:59:59Z",
          
          "date_dt_p", (1800 + i) + "-12-31T23:59:59Z",
          "date_dtm_p", (1800 + i) + "-12-31T23:59:59Z",
          "date_dtm_p", (1800 + i + 10) + "-12-31T23:59:59Z",
          "date_dtm_p", (1800 + i + 10) + "-12-31T23:59:59Z",
          "date_dtm_p", (1800 + i + 20) + "-12-31T23:59:59Z",
          
          "string_s", "abc" + i,
          "string_sm", "abc" + i,
          "string_sm", "def" + i,
          "string_sm", "def" + i,
          "string_sm", "ghi" + i,
          
          "boolean_b", Boolean.toString(i % 3 == 0),
          "boolean_bm", "false",
          "boolean_bm", "true",
          "boolean_bm", "false"
      ));
      
      singleInts.put(""+i, i);
      Map<Integer, Integer> ints = new HashMap<>();
      ints.put(i, 1);
      ints.put(i + 10, 2);
      ints.put(i + 20, 1);
      multiInts.put(""+i, ints);
      
      singleLongs.put(""+i, (long) i);
      Map<Long, Integer> longs = new HashMap<>();
      longs.put((long) i, 1);
      longs.put(i + 10L, 2);
      longs.put(i + 20L, 1);
      multiLongs.put(""+i, longs);
      
      singleFloats.put(""+i, i + .75F);
      Map<Float, Integer> floats = new HashMap<>();
      floats.put(i + .75F, 1);
      floats.put(i + 10.75F, 2);
      floats.put(i + 20.75F, 1);
      multiFloats.put(""+i, floats);
      
      singleDoubles.put(""+i, i + .5);
      Map<Double, Integer> doubles = new HashMap<>();
      doubles.put(i + .5, 1);
      doubles.put(i + 10.5, 2);
      doubles.put(i + 20.5, 1);
      multiDoubles.put(""+i, doubles);
      
      singleDates.put(""+i, Instant.parse((1800 + i) + "-12-31T23:59:59Z").toEpochMilli());
      Map<Long, Integer> dates = new HashMap<>();
      dates.put(Instant.parse((1800 + i) + "-12-31T23:59:59Z").toEpochMilli(), 1);
      dates.put(Instant.parse((1800 + i + 10) + "-12-31T23:59:59Z").toEpochMilli(), 2);
      dates.put(Instant.parse((1800 + i + 20) + "-12-31T23:59:59Z").toEpochMilli(), 1);
      multiDates.put(""+i, dates);
      
      singleStrings.put(""+i, "abc" + i);
      Map<String, Integer> strings = new HashMap<>();
      strings.put("abc" + i, 1);
      strings.put("def" + i, 2);
      strings.put("ghi" + i, 1);
      multiStrings.put(""+i, strings);
      
      singleBooleans.put(""+i, i % 3 == 0);
      Map<Boolean, Integer> booleans = new HashMap<>();
      booleans.put(true, 1);
      booleans.put(false, 2);
      multiBooleans.put(""+i, booleans);
    }
    assertU(commit());
    
    ref = h.getCore().getSearcher();
    searcher = ref.get();
    
    indexSchema = h.getCore().getLatestSchema();
  }
  
  protected ExpressionFactory getExpressionFactory() {
    ExpressionFactory fact = new ExpressionFactory(indexSchema);
    fact.startRequest();
    return fact;
  }
  
  @AfterClass
  public static void closeSearcher() throws IOException {
    ref.decref();
  }
  
  protected <T> void checkSingleFieldValues(Map<String,T> expected, Map<String,T> found, Set<String> missing) {
    expected.forEach( (id, value) -> {
      assertTrue("Field does not contain value for id '" + id + "'.", found.containsKey(id));
      assertEquals(value, found.get(id));
    });    
    assertEquals(expected.size(), found.size());
    
    missingDocuments.forEach( id -> assertTrue("Field does not have correct information for missing id '" + id + "'.", missing.contains(id)));
    assertEquals(missingDocuments.size(), missing.size());
  }
  
  protected <T> void checkMultiFieldValues(Map<String,Map<T,Integer>> expected, Map<String,Map<T,Integer>> found, Set<String> missing, boolean deduplicated) {
    expected.forEach( (id, expectedValues) -> {
      assertTrue("Field does not contain values for id '" + id + "'.", found.containsKey(id));
      Map<T,Integer> foundValues = found.get(id);
      expectedValues.forEach( (value, count) -> {
        assertTrue("Value '" + value + "' not found for document with id '" + id + "'.", foundValues.containsKey(value));
        if (deduplicated) {
          assertEquals(1, foundValues.get(value).intValue());
        } else {
          assertEquals(count, foundValues.get(value));
        }
      });
      assertEquals(expectedValues.size(), foundValues.size());
    });
    assertEquals(expected.size(), found.size());
    
    missingDocuments.forEach( id -> assertTrue("Field does not have correct information for missing id '" + id + "'.", missing.contains(id)));
    assertEquals(missingDocuments.size(), missing.size());
  }
  
  protected boolean emptyValueFound;
  
  protected Set<String> collectFieldValues(AnalyticsField testField, Predicate<String> valuesFiller) throws IOException {
    StringField idField = new StringField("id");
    Set<String> missing = new HashSet<>();
    
    List<LeafReaderContext> contexts = searcher.getTopReaderContext().leaves();
    for (LeafReaderContext context : contexts) {
      testField.doSetNextReader(context);
      idField.doSetNextReader(context);
      Bits liveDocs = context.reader().getLiveDocs();
      for (int doc = 0; doc < context.reader().maxDoc(); doc++) {
        if (liveDocs != null && !liveDocs.get(doc)) {
          continue;
        }
        // Add a document to the statistics being generated
        testField.collect(doc);
        idField.collect(doc);

        String id = idField.getString();
        if (!valuesFiller.test(id)) {
          missing.add(id);
        }
      }
    }
    return missing;
  }
}
