/*
 * 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.
 */
/*
 * StructSetOrResultsSet.java Utlity Class : Can be used to compare the results (StructSet OR
 * ResultsSet) under the scenario without/with Index Usage. Created on June 13, 2005, 11:16 AM
 */
package org.apache.geode.cache.query.functional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.apache.geode.LogWriter;
import org.apache.geode.cache.query.CacheUtils;
import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.Query;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.SelectResults;
import org.apache.geode.cache.query.Struct;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.CompiledGroupBySelect;
import org.apache.geode.cache.query.internal.CompiledSelect;
import org.apache.geode.cache.query.internal.CompiledSortCriterion;
import org.apache.geode.cache.query.internal.CompiledValue;
import org.apache.geode.cache.query.internal.DefaultQuery;
import org.apache.geode.cache.query.internal.ExecutionContext;
import org.apache.geode.cache.query.internal.OrderByComparator;
import org.apache.geode.cache.query.internal.QueryObserverAdapter;
import org.apache.geode.cache.query.internal.QueryObserverHolder;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.internal.util.ArrayUtils;

/**
 * Used by these tests:
 * <ul>
 * <li>EquiJoinIntegrationTest</li>
 * <li>IUMRCompositeIteratorJUnitTest</li>
 * <li>IUMRMultiIndexesMultiRegionJUnitTest</li>
 * <li>IUMRShuffleIteratorsJUnitTest</li>
 * <li>IUMRSingleRegionJUnitTest</li>
 * <li>IndexCreationJUnitTest</li>
 * <li>IndexHintJUnitTest</li>
 * <li>IndexMaintainceJUnitTest</li>
 * <li>IndexUseJUnitTest</li>
 * <li>IndexedMergeEquiJoinScenariosJUnitTest</li>
 * <li>MultiRegionIndexUsageJUnitTest</li>
 * <li>NonDistinctOrderByPartitionedJUnitTest</li>
 * <li>NonDistinctOrderByReplicatedJUnitTest</li>
 * <li>NonDistinctOrderByTestImplementation</li>
 * <li>OrderByPartitionedJUnitTest</li>
 * <li>OrderByReplicatedJUnitTest</li>
 * <li>OrderByTestImplementation</li>
 * <li>QueryIndexUsingXMLDUnitTest</li>
 * <li>QueryREUpdateInProgressJUnitTest</li>
 * <li>QueryUsingFunctionContextDUnitTest</li>
 * <li>QueryUsingPoolDUnitTest</li>
 * <li>TestNewFunctionSSorRSIntegrationTest</li>
 * </ul>
 * Also used by:
 * <ul>
 * <li>GroupByTestImpl</li>
 * <li>PdxGroupByTestImpl</li>
 * <li>PRQueryDUnitHelper</li>
 * </ul>
 */
public class StructSetOrResultsSet {

  public void CompareQueryResultsWithoutAndWithIndexes(Object[][] r, int len, String[] queries) {
    CompareQueryResultsWithoutAndWithIndexes(r, len, false, queries);
  }

  /* Creates a new instance of StructSetOrResultsSet */
  public void CompareQueryResultsWithoutAndWithIndexes(Object[][] r, int len, boolean checkOrder,
      String[] queries) {

    Collection coll1;
    Collection coll2;
    for (int j = 0; j < len; j++) {
      checkSelectResultTypes((SelectResults) r[j][0], (SelectResults) r[j][1], queries[j]);
      checkResultSizes((SelectResults) r[j][0], (SelectResults) r[j][1], queries[j]);

      if (checkOrder) {
        coll2 = (((SelectResults) r[j][1]).asList());
        coll1 = (((SelectResults) r[j][0]).asList());
      } else {
        coll2 = (((SelectResults) r[j][1]).asSet());
        coll1 = (((SelectResults) r[j][0]).asSet());
      }
      compareResults(coll1, coll2, queries[j], checkOrder);
    }
  }

  public void compareExternallySortedQueriesWithOrderBy(String[] queries, Object[][] baseResults)
      throws Exception {
    for (int i = 0; i < queries.length; i++) {
      Query q;
      try {
        String query = queries[i];
        int indexOfOrderBy = query.indexOf("order ");
        query = query.substring(0, indexOfOrderBy);
        q = CacheUtils.getQueryService().newQuery(query);
        CacheUtils.getLogger().info("Executing query: " + query);
        baseResults[i][1] = q.execute();
        int unorderedResultsSize = ((SelectResults) baseResults[i][1]).size();
        if (unorderedResultsSize == 0) {
          fail(
              "The test results size is 0. It may not be validating anything. Please rewrite the test.");
        }
        Wrapper wrapper = getOrderByComparatorAndLimitForQuery(queries[i], unorderedResultsSize);
        if (wrapper.validationLevel != ValidationLevel.NONE) {
          Object[] externallySorted = ((SelectResults) baseResults[i][1]).toArray();
          if (wrapper.validationLevel != ValidationLevel.MATCH_ONLY) {
            Arrays.sort(externallySorted, wrapper.obc);
          }
          if (wrapper.limit != -1) {
            if (externallySorted.length > wrapper.limit) {
              Object[] newExternallySorted = new Object[wrapper.limit];
              System.arraycopy(externallySorted, 0, newExternallySorted, 0, wrapper.limit);
              externallySorted = newExternallySorted;
            }
          }
          StructSetOrResultsSet ssOrrs1 = new StructSetOrResultsSet();
          ssOrrs1.compareQueryResultsWithExternallySortedResults((SelectResults) baseResults[i][0],
              externallySorted, queries[i], wrapper);


        }
      } catch (Exception e) {
        e.printStackTrace();
        throw new AssertionError(
            "query with index=" + i + " has failed. failed query=" + queries[i], e);
      }
    }
  }

  private void compareQueryResultsWithExternallySortedResults(SelectResults sr,
      Object[] externallySorted, String query, Wrapper wrapper) {

    if (sr.size() == externallySorted.length) {
      CacheUtils.log("Both SelectResults are of Same Size i.e.  Size= " + sr.size());
    } else {
      fail("FAILED:SelectResults size is different in both the cases. Size1=" + sr.size()
          + " Size2 = " + externallySorted.length + "; failed query=" + query);
    }

    Collection coll1 = null;
    coll1 = sr.asList();
    int j = 0;
    if (wrapper.validationLevel != ValidationLevel.ORDER_BY_ONLY) {
      for (Object o : externallySorted) {
        boolean removed = coll1.remove(o);
        if (!removed) {
          LogWriter logger = CacheUtils.getLogger();
          logger.error("order by inconsistency at element index = " + j);
          logger.error(" query result =****");
          logger.error(" query result =" + coll1);
          logger.error(" query result elementType=" + sr.getCollectionType().getElementType());
          logger.error(" externally sorted result =****");
          logger.error(ArrayUtils.toString(externallySorted));

          fail("failed query due to element mismatch=" + query);
        }
        ++j;
      }
      assertTrue(coll1.isEmpty());
    }

    if (wrapper.validationLevel != ValidationLevel.MATCH_ONLY) {
      coll1 = sr.asList();
      Iterator itert1 = coll1.iterator();
      int i = 0;
      for (Object o : externallySorted) {

        if (wrapper.obc.compare(o, itert1.next()) != 0) {
          LogWriter logger = CacheUtils.getLogger();
          logger.error("order by inconsistency at element index = " + i);
          logger.error(" query result =****");
          logger.error(" query result =" + coll1);
          logger.error(" externally sorted result =****");
          logger.error(ArrayUtils.toString(externallySorted));
          fail("failed query due to order mismatch=" + query);
        }
        ++i;
      }
    }
  }

  public Wrapper getOrderByComparatorAndLimitForQuery(String orderByQuery, int unorderedResultSize)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException, NoSuchFieldException, SecurityException,
      IllegalArgumentException, IllegalAccessException, NoSuchMethodException,
      InvocationTargetException {
    DefaultQuery q = (DefaultQuery) CacheUtils.getQueryService().newQuery(orderByQuery);
    CompiledSelect cs = q.getSimpleSelect();
    List<CompiledSortCriterion> orderByAttribs = null;
    if (cs.getType() == CompiledValue.GROUP_BY_SELECT) {
      Field originalOrderByMethod =
          CompiledGroupBySelect.class.getDeclaredField("originalOrderByClause");
      originalOrderByMethod.setAccessible(true);
      orderByAttribs = (List<CompiledSortCriterion>) originalOrderByMethod.get(cs);
    } else {
      orderByAttribs = cs.getOrderByAttrs();
    }
    ObjectType resultType = cs.getElementTypeForOrderByQueries();
    ExecutionContext context = new ExecutionContext(null, CacheUtils.getCache());
    final OrderByComparator obc = new OrderByComparator(orderByAttribs, resultType, context);
    Comparator baseComparator = obc;
    if (resultType.isStructType()) {
      baseComparator =
          (Comparator<Struct>) (o1, o2) -> obc.compare(o1.getFieldValues(), o2.getFieldValues());
    }
    final Comparator secondLevelComparator = baseComparator;
    final Comparator finalComparator = (o1, o2) -> {
      final boolean[] orderByColsEqual = new boolean[] {false};
      QueryObserverHolder.setInstance(new QueryObserverAdapter() {
        @Override
        public void orderByColumnsEqual() {
          orderByColsEqual[0] = true;
        }
      });
      int result = secondLevelComparator.compare(o1, o2);
      if (result != 0 && orderByColsEqual[0]) {
        result = 0;
      }
      return result;
    };

    Field hasUnmappedOrderByColsField =
        CompiledSelect.class.getDeclaredField("hasUnmappedOrderByCols");
    hasUnmappedOrderByColsField.setAccessible(true);
    boolean skip = (Boolean) hasUnmappedOrderByColsField.get(cs);
    ValidationLevel validationLevel = ValidationLevel.ALL;

    int limit;

    if (cs.getType() == CompiledValue.GROUP_BY_SELECT) {
      Field limitCVField = CompiledGroupBySelect.class.getDeclaredField("limit");
      limitCVField.setAccessible(true);
      CompiledValue limitCV = (CompiledValue) limitCVField.get(cs);
      Method evaluateLimitMethod = CompiledSelect.class.getDeclaredMethod("evaluateLimitValue",
          ExecutionContext.class, CompiledValue.class);
      evaluateLimitMethod.setAccessible(true);
      limit = (Integer) evaluateLimitMethod.invoke(null, context, limitCV);
    } else {
      limit = cs.getLimitValue(null);
    }

    if (limit != -1 && limit < unorderedResultSize) {
      // chances are that results will not match
      if (skip) {
        validationLevel = ValidationLevel.NONE;
      } else {
        validationLevel = ValidationLevel.ORDER_BY_ONLY;
      }
    } else {
      if (skip) {
        validationLevel = ValidationLevel.MATCH_ONLY;
      }
    }
    return new Wrapper(finalComparator, limit, validationLevel);
  }

  enum ValidationLevel {
    ALL, ORDER_BY_ONLY, MATCH_ONLY, NONE
  }

  private static class Wrapper {
    final Comparator obc;
    final int limit;
    final ValidationLevel validationLevel;

    Wrapper(Comparator obc, int limit, ValidationLevel validationLevel) {
      this.obc = obc;
      this.limit = limit;
      this.validationLevel = validationLevel;
    }
  }

  /* Creates a new instance of StructSetOrResultsSet */
  public void CompareCountStarQueryResultsWithoutAndWithIndexes(Object[][] r, int len,
      boolean checkOrder, String[] queries) {

    Integer count1, count2;
    Iterator<Integer> itert1, itert2;
    SelectResults result1, result2;
    boolean exactMatch = true;
    for (int j = 0; j < len; j++) {
      result1 = ((SelectResults) r[j][0]);
      result2 = ((SelectResults) r[j][1]);
      assertEquals(queries[j], 1, result1.size());
      assertEquals(queries[j], 1, result2.size());

      checkSelectResultTypes((SelectResults) r[j][0], (SelectResults) r[j][1], queries[j]);
      checkResultSizes((SelectResults) r[j][0], (SelectResults) r[j][1], queries[j]);
      compareResults(result1, result2, queries[j], true);
    }
  }

  /*
   * Compares two ArrayLists containing query results with/without order.
   */
  public void CompareQueryResultsAsListWithoutAndWithIndexes(Object[][] r, int len,
      boolean checkOrder, String[] queries) {
    CompareQueryResultsAsListWithoutAndWithIndexes(r, len, checkOrder, true, queries);
  }

  public void CompareQueryResultsAsListWithoutAndWithIndexes(Object[][] r, int len,
      boolean checkOrder, boolean checkClass, String[] queries) {
    Integer count1, count2;
    Iterator<Integer> itert1, itert2;
    ArrayList result1, result2;

    for (int j = 0; j < len; j++) {
      result1 = ((ArrayList) r[j][0]);
      result2 = ((ArrayList) r[j][1]);
      result1.trimToSize();
      result2.trimToSize();

      compareQueryResultLists(result1, result2, len, checkOrder, checkClass, queries[j]);
    }
  }

  public void compareQueryResultLists(List r1, List r2, int len, boolean checkOrder,
      boolean checkClass, String query) {
    if (checkClass) {
      if ((r1.get(0).getClass().getName()).equals(r2.get(0).getClass().getName())) {
        CacheUtils.log(
            "Both SelectResults are of the same Type i.e.--> " + r1.get(0).getClass().getName());
      } else {
        fail("FAILED:Select result Type is different in both the cases."
            + r1.get(0).getClass().getName() + "and" + r1.get(0).getClass().getName()
            + "; failed query=" + query);
      }
    }

    checkResultSizes(r1, r2, query);
    compareResults(r1, r2, query, checkOrder);
  }

  private void checkSelectResultTypes(SelectResults r1, SelectResults r2, String query) {
    ObjectType type1, type2;
    type1 = r1.getCollectionType().getElementType();
    type2 = r2.getCollectionType().getElementType();
    if (!(type1.getClass().getName()).equals(type2.getClass().getName())) {
      CacheUtils
          .log("Classes are : " + type1.getClass().getName() + " " + type2.getClass().getName());
      fail("FAILED:Select result Type is different in both the cases." + "; failed query=" + query);
    }
  }

  private void checkResultSizes(Collection r1, Collection r2, String query) {
    if (r1.size() != r2.size()) {
      fail("FAILED:SelectResults size is different in both the cases. Size1=" + r1.size()
          + " Size2 = " + r2.size() + "; failed query=" + query);
    }
  }

  private void compareResults(Collection result1, Collection result2, String query,
      boolean checkOrder) {
    Iterator itert1 = result1.iterator();
    Iterator itert2 = result2.iterator();
    int currentIndex = 0;
    while (itert1.hasNext()) {
      Object p1 = itert1.next();
      Object p2 = null;
      if (!checkOrder) {
        if (!collectionContains(result2, p1)) {
          fail("Atleast one element in the pair of SelectResults "
              + "supposedly identical, is not equal " + "Match not found for :" + p1
              + " compared with:" + p2 + "; failed query=" + query + "; element unmatched =" + p1
              + ";p1 class=" + p1.getClass() + " ; other set has =" + result2);
        }
      } else {
        boolean matched = false;
        if (itert2.hasNext()) {
          p2 = itert2.next();
          matched = objectsEqual(p1, p2);
          if (!matched) {
            fail("Order of results was not the same, match not found for :" + p1 + " compared with:"
                + p2 + "; failed query=" + query + "; element unmatched =" + p1 + ";p1 class="
                + p1.getClass() + " compared with " + p2 + ";p2 class=" + p2.getClass()
                + "currentIndex:" + currentIndex + "\nr1:" + result1 + "\n\nr2:" + result2);
          }
        }
      }
      currentIndex++;
    }
  }

  private boolean collectionContains(Collection collection, Object object) {
    for (final Object o : collection) {
      if (objectsEqual(object, o)) {
        return true;
      }
    }
    return false;
  }

  private boolean objectsEqual(Object o1, Object o2) {
    // Assumed that o1 and o2 are the same object type as both are from collections created by
    // executing the same query
    if (o1 instanceof Struct) {
      // if o2 is null, an NPE will be thrown.
      Object[] values1 = ((Struct) o1).getFieldValues();
      Object[] values2 = ((Struct) o2).getFieldValues();
      assertEquals(values1.length, values2.length);
      boolean elementEqual = true;
      for (int i = 0; i < values1.length; ++i) {
        elementEqual =
            elementEqual && ((values1[i] == values2[i]) || values1[i].equals(values2[i]));
      }
      return elementEqual;
    } else {
      // if o1 is null and o2 is not, an NPE will be thrown
      return o1 == o2 || o1.equals(o2);
    }
  }
}
