/*
 * 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.lucene.luke.models.search;

import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleDocValuesField;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FloatDocValuesField;
import org.apache.lucene.document.FloatPoint;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.luke.models.LukeException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.PointRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortedNumericSortField;
import org.apache.lucene.search.SortedSetSortField;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.index.RandomIndexWriter;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
import org.junit.Test;

public class TestSearchImpl extends LuceneTestCase {

  private IndexReader reader;
  private Directory dir;
  private Path indexDir;

  @Override
  public void setUp() throws Exception {
    super.setUp();
    createIndex();
    dir = newFSDirectory(indexDir);
    reader = DirectoryReader.open(dir);
  }

  private void createIndex() throws IOException {
    indexDir = createTempDir("testIndex");

    Directory dir = newFSDirectory(indexDir);
    RandomIndexWriter writer = new RandomIndexWriter(random(), dir, new StandardAnalyzer());

    for (int i = 0; i < 10; i++) {
      Document doc1 = new Document();
      doc1.add(newTextField("f1", "Apple Pie", Field.Store.YES));
      doc1.add(new SortedDocValuesField("f2", new BytesRef("a" + (i * 10 + 1))));
      doc1.add(new SortedSetDocValuesField("f3", new BytesRef("a" + (i * 10 + 1))));
      doc1.add(new NumericDocValuesField("f4", i * 10 + 1L));
      doc1.add(new FloatDocValuesField("f5", i * 10 + 1.0f));
      doc1.add(new DoubleDocValuesField("f6", i * 10 + 1.0));
      doc1.add(new SortedNumericDocValuesField("f7", i * 10 + 1L));
      doc1.add(new IntPoint("f8", i * 10 + 1));
      doc1.add(new LongPoint("f9", i * 10 + 1L));
      doc1.add(new FloatPoint("f10", i * 10 + 1.0f));
      doc1.add(new DoublePoint("f11", i * 10 + 1.0));
      writer.addDocument(doc1);

      Document doc2 = new Document();
      doc2.add(newTextField("f1", "Brownie", Field.Store.YES));
      doc2.add(new SortedDocValuesField("f2", new BytesRef("b" + (i * 10 + 2))));
      doc2.add(new SortedSetDocValuesField("f3", new BytesRef("b" + (i * 10 + 2))));
      doc2.add(new NumericDocValuesField("f4", i * 10 + 2L));
      doc2.add(new FloatDocValuesField("f5", i * 10 + 2.0f));
      doc2.add(new DoubleDocValuesField("f6", i * 10 + 2.0));
      doc2.add(new SortedNumericDocValuesField("f7", i * 10 + 2L));
      doc2.add(new IntPoint("f8", i * 10 + 2));
      doc2.add(new LongPoint("f9", i * 10 + 2L));
      doc2.add(new FloatPoint("f10", i * 10 + 2.0f));
      doc2.add(new DoublePoint("f11", i * 10 + 2.0));
      writer.addDocument(doc2);

      Document doc3 = new Document();
      doc3.add(newTextField("f1", "Chocolate Pie", Field.Store.YES));
      doc3.add(new SortedDocValuesField("f2", new BytesRef("c" + (i * 10 + 3))));
      doc3.add(new SortedSetDocValuesField("f3", new BytesRef("c" + (i * 10 + 3))));
      doc3.add(new NumericDocValuesField("f4", i * 10 + 3L));
      doc3.add(new FloatDocValuesField("f5", i * 10 + 3.0f));
      doc3.add(new DoubleDocValuesField("f6", i * 10 + 3.0));
      doc3.add(new SortedNumericDocValuesField("f7", i * 10 + 3L));
      doc3.add(new IntPoint("f8", i * 10 + 3));
      doc3.add(new LongPoint("f9", i * 10 + 3L));
      doc3.add(new FloatPoint("f10", i * 10 + 3.0f));
      doc3.add(new DoublePoint("f11", i * 10 + 3.0));
      writer.addDocument(doc3);

      Document doc4 = new Document();
      doc4.add(newTextField("f1", "Doughnut", Field.Store.YES));
      doc4.add(new SortedDocValuesField("f2", new BytesRef("d" + (i * 10 + 4))));
      doc4.add(new SortedSetDocValuesField("f3", new BytesRef("d" + (i * 10 + 4))));
      doc4.add(new NumericDocValuesField("f4", i * 10 + 4L));
      doc4.add(new FloatDocValuesField("f5", i * 10 + 4.0f));
      doc4.add(new DoubleDocValuesField("f6", i * 10 + 4.0));
      doc4.add(new SortedNumericDocValuesField("f7", i * 10 + 4L));
      doc4.add(new IntPoint("f8", i * 10 + 4));
      doc4.add(new LongPoint("f9", i * 10 + 4L));
      doc4.add(new FloatPoint("f10", i * 10 + 4.0f));
      doc4.add(new DoublePoint("f11", i * 10 + 4.0));
      writer.addDocument(doc4);

      Document doc5 = new Document();
      doc5.add(newTextField("f1", "Eclair", Field.Store.YES));
      doc5.add(new SortedDocValuesField("f2", new BytesRef("e" + (i * 10 + 5))));
      doc5.add(new SortedSetDocValuesField("f3", new BytesRef("e" + (i * 10 + 5))));
      doc5.add(new NumericDocValuesField("f4", i * 10 + 5L));
      doc5.add(new FloatDocValuesField("f5", i * 10 + 5.0f));
      doc5.add(new DoubleDocValuesField("f6", i * 10 + 5.0));
      doc5.add(new SortedNumericDocValuesField("f7", i * 10 + 5L));
      doc5.add(new IntPoint("f8", i * 10 + 5));
      doc5.add(new LongPoint("f9", i * 10 + 5L));
      doc5.add(new FloatPoint("f10", i * 10 + 5.0f));
      doc5.add(new DoublePoint("f11", i * 10 + 5.0));
      writer.addDocument(doc5);
    }
    writer.commit();
    writer.close();
    dir.close();
  }

  @Override
  public void tearDown() throws Exception {
    super.tearDown();
    reader.close();
    dir.close();
  }

  @Test
  public void testGetSortableFieldNames() {
    SearchImpl search = new SearchImpl(reader);
    assertArrayEquals(
        new String[] {"f2", "f3", "f4", "f5", "f6", "f7"},
        search.getSortableFieldNames().toArray());
  }

  @Test
  public void testGetSearchableFieldNames() {
    SearchImpl search = new SearchImpl(reader);
    assertArrayEquals(new String[] {"f1"}, search.getSearchableFieldNames().toArray());
  }

  @Test
  public void testGetRangeSearchableFieldNames() {
    SearchImpl search = new SearchImpl(reader);
    assertArrayEquals(
        new String[] {"f8", "f9", "f10", "f11"}, search.getRangeSearchableFieldNames().toArray());
  }

  @Test
  public void testParseClassic() {
    SearchImpl search = new SearchImpl(reader);
    QueryParserConfig config =
        new QueryParserConfig.Builder()
            .allowLeadingWildcard(true)
            .defaultOperator(QueryParserConfig.Operator.AND)
            .fuzzyMinSim(1.0f)
            .build();
    Query q = search.parseQuery("app~ f2:*ie", "f1", new StandardAnalyzer(), config, false);
    assertEquals("+f1:app~1 +f2:*ie", q.toString());
  }

  @Test
  public void testParsePointRange() {
    SearchImpl search = new SearchImpl(reader);
    Map<String, Class<? extends Number>> types = new HashMap<>();
    types.put("f8", Integer.class);

    QueryParserConfig config =
        new QueryParserConfig.Builder().useClassicParser(false).typeMap(types).build();
    Query q = search.parseQuery("f8:[10 TO 20]", "f1", new StandardAnalyzer(), config, false);
    assertEquals("f8:[10 TO 20]", q.toString());
    assertTrue(q instanceof PointRangeQuery);
  }

  @Test
  public void testGuessSortTypes() {
    SearchImpl search = new SearchImpl(reader);

    assertTrue(search.guessSortTypes("f1").isEmpty());

    assertArrayEquals(
        new SortField[] {
          new SortField("f2", SortField.Type.STRING), new SortField("f2", SortField.Type.STRING_VAL)
        },
        search.guessSortTypes("f2").toArray());

    assertArrayEquals(
        new SortField[] {new SortedSetSortField("f3", false)},
        search.guessSortTypes("f3").toArray());

    assertArrayEquals(
        new SortField[] {
          new SortField("f4", SortField.Type.INT),
          new SortField("f4", SortField.Type.LONG),
          new SortField("f4", SortField.Type.FLOAT),
          new SortField("f4", SortField.Type.DOUBLE)
        },
        search.guessSortTypes("f4").toArray());

    assertArrayEquals(
        new SortField[] {
          new SortField("f5", SortField.Type.INT),
          new SortField("f5", SortField.Type.LONG),
          new SortField("f5", SortField.Type.FLOAT),
          new SortField("f5", SortField.Type.DOUBLE)
        },
        search.guessSortTypes("f5").toArray());

    assertArrayEquals(
        new SortField[] {
          new SortField("f6", SortField.Type.INT),
          new SortField("f6", SortField.Type.LONG),
          new SortField("f6", SortField.Type.FLOAT),
          new SortField("f6", SortField.Type.DOUBLE)
        },
        search.guessSortTypes("f6").toArray());

    assertArrayEquals(
        new SortField[] {
          new SortedNumericSortField("f7", SortField.Type.INT),
          new SortedNumericSortField("f7", SortField.Type.LONG),
          new SortedNumericSortField("f7", SortField.Type.FLOAT),
          new SortedNumericSortField("f7", SortField.Type.DOUBLE)
        },
        search.guessSortTypes("f7").toArray());
  }

  @Test(expected = LukeException.class)
  public void testGuessSortTypesNoSuchField() {
    SearchImpl search = new SearchImpl(reader);
    search.guessSortTypes("unknown");
  }

  @Test
  public void testGetSortType() {
    SearchImpl search = new SearchImpl(reader);

    assertFalse(search.getSortType("f1", "STRING", false).isPresent());

    assertEquals(
        new SortField("f2", SortField.Type.STRING, false),
        search.getSortType("f2", "STRING", false).get());
    assertFalse(search.getSortType("f2", "INT", false).isPresent());

    assertEquals(
        new SortedSetSortField("f3", false), search.getSortType("f3", "CUSTOM", false).get());

    assertEquals(
        new SortField("f4", SortField.Type.LONG, false),
        search.getSortType("f4", "LONG", false).get());
    assertFalse(search.getSortType("f4", "STRING", false).isPresent());

    assertEquals(
        new SortField("f5", SortField.Type.FLOAT, false),
        search.getSortType("f5", "FLOAT", false).get());
    assertFalse(search.getSortType("f5", "STRING", false).isPresent());

    assertEquals(
        new SortField("f6", SortField.Type.DOUBLE, false),
        search.getSortType("f6", "DOUBLE", false).get());
    assertFalse(search.getSortType("f6", "STRING", false).isPresent());

    assertEquals(
        new SortedNumericSortField("f7", SortField.Type.LONG, false),
        search.getSortType("f7", "LONG", false).get());
    assertFalse(search.getSortType("f7", "STRING", false).isPresent());
  }

  @Test(expected = LukeException.class)
  public void testGetSortTypeNoSuchField() {
    SearchImpl search = new SearchImpl(reader);

    search.getSortType("unknown", "STRING", false);
  }

  @Test
  public void testSearch() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("apple");
    SearchResults res =
        search.search(query, new SimilarityConfig.Builder().build(), null, 10, true);

    assertEquals(10, res.getTotalHits().value);
    assertEquals(10, res.size());
    assertEquals(0, res.getOffset());
  }

  @Test
  public void testSearchWithSort() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("apple");
    Sort sort = new Sort(new SortField("f2", SortField.Type.STRING, true));
    SearchResults res =
        search.search(query, new SimilarityConfig.Builder().build(), sort, null, 10, true);

    assertEquals(10, res.getTotalHits().value);
    assertEquals(10, res.size());
    assertEquals(0, res.getOffset());
  }

  @Test
  public void testNextPage() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("pie");
    search.search(query, new SimilarityConfig.Builder().build(), null, 10, true);
    Optional<SearchResults> opt = search.nextPage();
    assertTrue(opt.isPresent());

    SearchResults res = opt.get();
    assertEquals(20, res.getTotalHits().value);
    assertEquals(10, res.size());
    assertEquals(10, res.getOffset());
  }

  @Test(expected = LukeException.class)
  public void testNextPageSearchNotStarted() {
    SearchImpl search = new SearchImpl(reader);
    search.nextPage();
  }

  @Test
  public void testNextPageNoMoreResults() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("pie");
    search.search(query, new SimilarityConfig.Builder().build(), null, 10, true);
    search.nextPage();
    assertFalse(search.nextPage().isPresent());
  }

  @Test
  public void testPrevPage() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("pie");
    search.search(query, new SimilarityConfig.Builder().build(), null, 10, true);
    search.nextPage();
    Optional<SearchResults> opt = search.prevPage();
    assertTrue(opt.isPresent());

    SearchResults res = opt.get();
    assertEquals(20, res.getTotalHits().value);
    assertEquals(10, res.size());
    assertEquals(0, res.getOffset());
  }

  @Test(expected = LukeException.class)
  public void testPrevPageSearchNotStarted() {
    SearchImpl search = new SearchImpl(reader);
    search.prevPage();
  }

  @Test
  public void testPrevPageNoMoreResults() throws Exception {
    SearchImpl search = new SearchImpl(reader);
    Query query = new QueryParser("f1", new StandardAnalyzer()).parse("pie");
    search.search(query, new SimilarityConfig.Builder().build(), null, 10, true);
    assertFalse(search.prevPage().isPresent());
  }
}
