/*
 * 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.spatial.prefix;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.locationtech.spatial4j.shape.Shape;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.StrategyTestCase;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;

import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;

/** Base test harness, ideally for SpatialStrategy impls that have exact results
 * (not grid approximated), hence "not fuzzy".
 */
public abstract class RandomSpatialOpStrategyTestCase extends StrategyTestCase {

  //Note: this is partially redundant with StrategyTestCase.runTestQuery & testOperation

  protected void testOperationRandomShapes(final SpatialOperation operation) throws IOException {

    final int numIndexedShapes = randomIntBetween(1, 6);
    List<Shape> indexedShapes = new ArrayList<>(numIndexedShapes);
    for (int i = 0; i < numIndexedShapes; i++) {
      indexedShapes.add(randomIndexedShape());
    }

    final int numQueryShapes = atLeast(20);
    List<Shape> queryShapes = new ArrayList<>(numQueryShapes);
    for (int i = 0; i < numQueryShapes; i++) {
      queryShapes.add(randomQueryShape());
    }

    testOperation(operation, indexedShapes, queryShapes, true/*havoc*/);
  }

  protected void testOperation(final SpatialOperation operation,
                               List<Shape> indexedShapes, List<Shape> queryShapes, boolean havoc) throws IOException {
    //first show that when there's no data, a query will result in no results
    {
      Query query = strategy.makeQuery(new SpatialArgs(operation, randomQueryShape()));
      SearchResults searchResults = executeQuery(query, 1);
      assertEquals(0, searchResults.numFound);
    }

    //Main index loop:
    for (int i = 0; i < indexedShapes.size(); i++) {
      Shape shape = indexedShapes.get(i);
      adoc(""+i, shape);

      if (havoc && random().nextInt(10) == 0)
        commit();//intermediate commit, produces extra segments
    }
    if (havoc) {
      //delete some documents randomly
      for (int id = 0; id < indexedShapes.size(); id++) {
        if (random().nextInt(10) == 0) {
          deleteDoc(""+id);
          indexedShapes.set(id, null);
        }
      }
    }

    commit();

    //Main query loop:
    for (int queryIdx = 0; queryIdx < queryShapes.size(); queryIdx++) {
      final Shape queryShape = queryShapes.get(queryIdx);

      if (havoc)
        preQueryHavoc();

      //Generate truth via brute force:
      // We ensure true-positive matches (if the predicate on the raw shapes match
      //  then the search should find those same matches).
      Set<String> expectedIds = new LinkedHashSet<>();//true-positives
      for (int id = 0; id < indexedShapes.size(); id++) {
        Shape indexedShape = indexedShapes.get(id);
        if (indexedShape == null)
          continue;
        if (operation.evaluate(indexedShape, queryShape)) {
          expectedIds.add(""+id);
        }
      }

      //Search and verify results
      SpatialArgs args = new SpatialArgs(operation, queryShape);
      Query query = strategy.makeQuery(args);
      SearchResults got = executeQuery(query, 100);
      Set<String> remainingExpectedIds = new LinkedHashSet<>(expectedIds);
      for (SearchResult result : got.results) {
        String id = result.getId();
        if (!remainingExpectedIds.remove(id)) {
          fail("qIdx:" + queryIdx + " Shouldn't match", id, indexedShapes, queryShape, operation);
        }
      }
      if (!remainingExpectedIds.isEmpty()) {
        String id = remainingExpectedIds.iterator().next();
        fail("qIdx:" + queryIdx + " Should have matched", id, indexedShapes, queryShape, operation);
      }
    }
  }

  private void fail(String label, String id, List<Shape> indexedShapes, Shape queryShape, SpatialOperation operation) {
    fail("[" + operation + "] " + label
        + " I#" + id + ":" + indexedShapes.get(Integer.parseInt(id)) + " Q:" + queryShape);
  }

  protected void preQueryHavoc() {
    if (strategy instanceof RecursivePrefixTreeStrategy) {
      RecursivePrefixTreeStrategy rpts = (RecursivePrefixTreeStrategy) strategy;
      int scanLevel = randomIntBetween(0, rpts.getGrid().getMaxLevels());
      rpts.setPrefixGridScanLevel(scanLevel);
    }
  }

  protected abstract Shape randomIndexedShape();

  protected abstract Shape randomQueryShape();
}