/**
 * 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.hadoop.hive.ql.io.sarg;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;

import com.google.common.collect.Sets;

import org.apache.hadoop.hive.common.type.HiveChar;
import org.apache.hadoop.hive.common.type.HiveVarchar;
import org.apache.hadoop.hive.ql.io.orc.TestInputOutputFormat;
import org.apache.hadoop.hive.ql.io.sarg.SearchArgument.TruthValue;
import org.apache.hadoop.hive.ql.io.sarg.SearchArgumentImpl.PredicateLeafImpl;
import org.apache.hadoop.hive.serde2.io.HiveDecimalWritable;
import org.junit.Test;

import java.lang.reflect.Field;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.List;
import java.util.Set;

/**
 * These test the SARG implementation.
 * The xml files were generated by setting hive.optimize.index.filter
 * to true and using a custom record reader that prints out the value of
 * hive.io.filter.expr.serialized in createRecordReader. This should be
 * replaced by generating the AST using the API and passing that in.
 * <p/>
 * In each case, the corresponding part of the where clause is in the
 * comment above the blob.
 */
@SuppressWarnings("deprecation")
public class TestSearchArgumentImpl {

  private ExpressionTree not(ExpressionTree arg) {
    return new ExpressionTree(ExpressionTree.Operator.NOT, arg);
  }

  private ExpressionTree and(ExpressionTree... arg) {
    return new ExpressionTree(ExpressionTree.Operator.AND, arg);
  }

  private ExpressionTree or(ExpressionTree... arg) {
    return new ExpressionTree(ExpressionTree.Operator.OR, arg);
  }

  private ExpressionTree leaf(int leaf) {
    return new ExpressionTree(leaf);
  }

  private ExpressionTree constant(TruthValue val) {
    return new ExpressionTree(val);
  }

  /**
   * Create a predicate leaf. This is used by another test.
   */
  public static PredicateLeaf createPredicateLeaf(PredicateLeaf.Operator operator,
                                                  PredicateLeaf.Type type,
                                                  String columnName,
                                                  Object literal,
                                                  List<Object> literalList) {
    return new SearchArgumentImpl.PredicateLeafImpl(operator, type, columnName,
        literal, literalList, null);
  }

  @Test
  public void testNotPushdown() throws Exception {
    assertEquals("leaf-1", SearchArgumentImpl.BuilderImpl.pushDownNot(leaf(1))
        .toString());
    assertEquals("(not leaf-1)",
        SearchArgumentImpl.BuilderImpl.pushDownNot(not(leaf(1))).toString());
    assertEquals("leaf-1",
        SearchArgumentImpl.BuilderImpl.pushDownNot(not(not(leaf(1))))
            .toString());
    assertEquals("(not leaf-1)",
        SearchArgumentImpl.BuilderImpl.pushDownNot(not(not(not(leaf(1))))).
            toString());
    assertEquals("(or leaf-1 (not leaf-2))",
        SearchArgumentImpl.BuilderImpl.pushDownNot(not(and(not(leaf(1)),
            leaf(2)))).toString());
    assertEquals("(and (not leaf-1) leaf-2)",
        SearchArgumentImpl.BuilderImpl.pushDownNot(not(or(leaf(1),
            not(leaf(2))))).toString());
    assertEquals("(or (or (not leaf-1) leaf-2) leaf-3)",
        SearchArgumentImpl.BuilderImpl.pushDownNot(or(not(and(leaf(1),
                not(leaf(2)))),
            not(not(leaf(3))))).toString());
    assertEquals("NO", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.YES))).toString());
    assertEquals("YES", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.NO))).toString());
    assertEquals("NULL", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.NULL))).toString());
    assertEquals("YES_NO", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.YES_NO))).toString());
    assertEquals("YES_NULL", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.NO_NULL))).toString());
    assertEquals("NO_NULL", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.YES_NULL))).toString());
    assertEquals("YES_NO_NULL", SearchArgumentImpl.BuilderImpl.pushDownNot(
        not(constant(TruthValue.YES_NO_NULL))).toString());
  }

  @Test
  public void testFlatten() throws Exception {
    assertEquals("leaf-1", SearchArgumentImpl.BuilderImpl.flatten(leaf(1)).toString());
    assertEquals("NO",
        SearchArgumentImpl.BuilderImpl.flatten(constant(TruthValue.NO)).toString());
    assertEquals("(not (not leaf-1))",
        SearchArgumentImpl.BuilderImpl.flatten(not(not(leaf(1)))).toString());
    assertEquals("(and leaf-1 leaf-2)",
        SearchArgumentImpl.BuilderImpl.flatten(and(leaf(1), leaf(2))).toString());
    assertEquals("(and (or leaf-1 leaf-2) leaf-3)",
        SearchArgumentImpl.BuilderImpl.flatten(and(or(leaf(1), leaf(2)), leaf(3))
        ).toString());
    assertEquals("(and leaf-1 leaf-2 leaf-3 leaf-4)",
        SearchArgumentImpl.BuilderImpl.flatten(and(and(leaf(1), leaf(2)),
            and(leaf(3), leaf(4)))).toString());
    assertEquals("(or leaf-1 leaf-2 leaf-3 leaf-4)",
        SearchArgumentImpl.BuilderImpl.flatten(or(leaf(1), or(leaf(2), or(leaf(3),
            leaf(4))))).toString());
    assertEquals("(or leaf-1 leaf-2 leaf-3 leaf-4)",
        SearchArgumentImpl.BuilderImpl.flatten(or(or(or(leaf(1), leaf(2)), leaf(3)),
            leaf(4))).toString());
    assertEquals("(or leaf-1 leaf-2 leaf-3 leaf-4 leaf-5 leaf-6)",
        SearchArgumentImpl.BuilderImpl.flatten(or(or(leaf(1), or(leaf(2), leaf(3))),
            or(or(leaf(4), leaf(5)), leaf(6)))).toString());
    assertEquals("(and (not leaf-1) leaf-2 (not leaf-3) leaf-4 (not leaf-5) leaf-6)",
        SearchArgumentImpl.BuilderImpl.flatten(and(and(not(leaf(1)), and(leaf(2),
                not(leaf(3)))), and(and(leaf(4), not(leaf(5))), leaf(6)))
        ).toString());
    assertEquals("(not (and leaf-1 leaf-2 leaf-3))",
        SearchArgumentImpl.BuilderImpl.flatten(not(and(leaf(1), and(leaf(2), leaf(3))))
        ).toString());
  }

  @Test
  public void testFoldMaybe() throws Exception {
    assertEquals("(and leaf-1)",
        SearchArgumentImpl.BuilderImpl.foldMaybe(and(leaf(1),
            constant(TruthValue.YES_NO_NULL))).toString());
    assertEquals("(and leaf-1 leaf-2)",
        SearchArgumentImpl.BuilderImpl.foldMaybe(and(leaf(1),
            constant(TruthValue.YES_NO_NULL), leaf(2))).toString());
    assertEquals("(and leaf-1 leaf-2)",
        SearchArgumentImpl.BuilderImpl.
            foldMaybe(and(constant(TruthValue.YES_NO_NULL),
                leaf(1), leaf(2), constant(TruthValue.YES_NO_NULL))).toString());
    assertEquals("YES_NO_NULL",
        SearchArgumentImpl.BuilderImpl.
            foldMaybe(and(constant(TruthValue.YES_NO_NULL),
                constant(TruthValue.YES_NO_NULL))).toString());
    assertEquals("YES_NO_NULL",
        SearchArgumentImpl.BuilderImpl.
            foldMaybe(or(leaf(1),
                constant(TruthValue.YES_NO_NULL))).toString());
    assertEquals("(or leaf-1 (and leaf-2))",
        SearchArgumentImpl.BuilderImpl.foldMaybe(or(leaf(1),
            and(leaf(2), constant(TruthValue.YES_NO_NULL)))).toString());
    assertEquals("(and leaf-1)",
        SearchArgumentImpl.BuilderImpl.foldMaybe(and(or(leaf(2),
            constant(TruthValue.YES_NO_NULL)), leaf(1))).toString());
    assertEquals("(and leaf-100)", SearchArgumentImpl.BuilderImpl.foldMaybe(
        SearchArgumentImpl.BuilderImpl.convertToCNF(and(leaf(100),
            or(and(leaf(0), leaf(1)),
                and(leaf(2), leaf(3)),
                and(leaf(4), leaf(5)),
                and(leaf(6), leaf(7)),
                and(leaf(8), leaf(9)),
                and(leaf(10), leaf(11)),
                and(leaf(12), leaf(13)),
                and(leaf(14), leaf(15)),
                and(leaf(16), leaf(17)))))).toString());
  }

  @Test
  public void testCNF() throws Exception {
    assertEquals("leaf-1", SearchArgumentImpl.BuilderImpl.convertToCNF(leaf(1)).
        toString());
    assertEquals("NO", SearchArgumentImpl.BuilderImpl.convertToCNF(
        constant(TruthValue.NO)).toString());
    assertEquals("(not leaf-1)", SearchArgumentImpl.BuilderImpl.convertToCNF(
        not(leaf(1))).toString());
    assertEquals("(and leaf-1 leaf-2)", SearchArgumentImpl.BuilderImpl.
        convertToCNF(
            and(leaf(1), leaf(2))).toString());
    assertEquals("(or (not leaf-1) leaf-2)", SearchArgumentImpl.BuilderImpl.
        convertToCNF(
            or(not(leaf(1)), leaf(2))).toString());
    assertEquals("(and (or leaf-1 leaf-2) (not leaf-3))",
        SearchArgumentImpl.BuilderImpl.convertToCNF(
            and(or(leaf(1), leaf(2)), not(leaf(3)))).toString());
    assertEquals("(and (or leaf-1 leaf-3) (or leaf-2 leaf-3)" +
        " (or leaf-1 leaf-4) (or leaf-2 leaf-4))",
        SearchArgumentImpl.BuilderImpl.convertToCNF(
            or(and(leaf(1), leaf(2)), and(leaf(3), leaf(4)))).toString());
    assertEquals("(and" +
        " (or leaf-1 leaf-5) (or leaf-2 leaf-5)" +
        " (or leaf-3 leaf-5) (or leaf-4 leaf-5)" +
        " (or leaf-1 leaf-6) (or leaf-2 leaf-6)" +
        " (or leaf-3 leaf-6) (or leaf-4 leaf-6))",
        SearchArgumentImpl.BuilderImpl.convertToCNF(
            or(and(leaf(1), leaf(2), leaf(3), leaf(4)),
                and(leaf(5), leaf(6)))).toString());
    assertEquals("(and" +
        " (or leaf-5 leaf-6 (not leaf-7) leaf-1 leaf-3)" +
        " (or leaf-5 leaf-6 (not leaf-7) leaf-2 leaf-3)" +
        " (or leaf-5 leaf-6 (not leaf-7) leaf-1 leaf-4)" +
        " (or leaf-5 leaf-6 (not leaf-7) leaf-2 leaf-4))",
        SearchArgumentImpl.BuilderImpl.convertToCNF(
            or(and(leaf(1), leaf(2)),
                and(leaf(3), leaf(4)),
                or(leaf(5), leaf(6)),
                not(leaf(7)))).toString());
    assertEquals("(and" +
        " (or leaf-8 leaf-0 leaf-3 leaf-6)" +
        " (or leaf-8 leaf-1 leaf-3 leaf-6)" +
        " (or leaf-8 leaf-2 leaf-3 leaf-6)" +
        " (or leaf-8 leaf-0 leaf-4 leaf-6)" +
        " (or leaf-8 leaf-1 leaf-4 leaf-6)" +
        " (or leaf-8 leaf-2 leaf-4 leaf-6)" +
        " (or leaf-8 leaf-0 leaf-5 leaf-6)" +
        " (or leaf-8 leaf-1 leaf-5 leaf-6)" +
        " (or leaf-8 leaf-2 leaf-5 leaf-6)" +
        " (or leaf-8 leaf-0 leaf-3 leaf-7)" +
        " (or leaf-8 leaf-1 leaf-3 leaf-7)" +
        " (or leaf-8 leaf-2 leaf-3 leaf-7)" +
        " (or leaf-8 leaf-0 leaf-4 leaf-7)" +
        " (or leaf-8 leaf-1 leaf-4 leaf-7)" +
        " (or leaf-8 leaf-2 leaf-4 leaf-7)" +
        " (or leaf-8 leaf-0 leaf-5 leaf-7)" +
        " (or leaf-8 leaf-1 leaf-5 leaf-7)" +
        " (or leaf-8 leaf-2 leaf-5 leaf-7))",
        SearchArgumentImpl.BuilderImpl.convertToCNF(or(and(leaf(0), leaf(1),
                leaf(2)),
            and(leaf(3), leaf(4), leaf(5)),
            and(leaf(6), leaf(7)),
            leaf(8))).toString());
    assertEquals("YES_NO_NULL", SearchArgumentImpl.BuilderImpl.
        convertToCNF(or(and(leaf(0), leaf(1)),
            and(leaf(2), leaf(3)),
            and(leaf(4), leaf(5)),
            and(leaf(6), leaf(7)),
            and(leaf(8), leaf(9)),
            and(leaf(10), leaf(11)),
            and(leaf(12), leaf(13)),
            and(leaf(14), leaf(15)),
            and(leaf(16), leaf(17)))).toString());
    assertEquals("(and leaf-100 YES_NO_NULL)", SearchArgumentImpl.BuilderImpl.
        convertToCNF(and(leaf(100),
            or(and(leaf(0), leaf(1)),
                and(leaf(2), leaf(3)),
                and(leaf(4), leaf(5)),
                and(leaf(6), leaf(7)),
                and(leaf(8), leaf(9)),
                and(leaf(10), leaf(11)),
                and(leaf(12), leaf(13)),
                and(leaf(14), leaf(15)),
                and(leaf(16), leaf(17))))).toString());
    assertNoSharedNodes(SearchArgumentImpl.BuilderImpl.
        convertToCNF(or(and(leaf(0), leaf(1), leaf(2)),
            and(leaf(3), leaf(4), leaf(5)),
            and(leaf(6), leaf(7)),
            leaf(8))), Sets.<ExpressionTree>newIdentityHashSet());
  }

  private static void assertNoSharedNodes(ExpressionTree tree,
                                          Set<ExpressionTree> seen
  ) throws Exception {
    if (seen.contains(tree) &&
        tree.getOperator() != ExpressionTree.Operator.LEAF) {
      assertTrue("repeated node in expression " + tree, false);
    }
    seen.add(tree);
    if (tree.getChildren() != null) {
      for (ExpressionTree child : tree.getChildren()) {
        assertNoSharedNodes(child, seen);
      }
    }
  }

  @Test
  public void testBuilder() throws Exception {
    SearchArgument sarg =
        SearchArgumentFactory.newBuilder()
            .startAnd()
            .lessThan("x", PredicateLeaf.Type.LONG, 10L)
            .lessThanEquals("y", PredicateLeaf.Type.STRING, "hi")
            .equals("z", PredicateLeaf.Type.FLOAT, 1.0)
            .end()
            .build();
    assertEquals("leaf-0 = (LESS_THAN x 10), " +
        "leaf-1 = (LESS_THAN_EQUALS y hi), " +
        "leaf-2 = (EQUALS z 1.0), " +
        "expr = (and leaf-0 leaf-1 leaf-2)", sarg.toString());
    sarg = SearchArgumentFactory.newBuilder()
        .startNot()
        .startOr()
        .isNull("x", PredicateLeaf.Type.LONG)
        .between("y", PredicateLeaf.Type.LONG, 10L, 20L)
        .in("z", PredicateLeaf.Type.LONG, 1L, 2L, 3L)
        .nullSafeEquals("a", PredicateLeaf.Type.STRING, "stinger")
        .end()
        .end()
        .build();
    assertEquals("leaf-0 = (IS_NULL x), " +
        "leaf-1 = (BETWEEN y 10 20), " +
        "leaf-2 = (IN z 1 2 3), " +
        "leaf-3 = (NULL_SAFE_EQUALS a stinger), " +
        "expr = (and (not leaf-0) (not leaf-1) (not leaf-2) (not leaf-3))", sarg.toString());
  }

  @Test
  public void testBuilderComplexTypes() throws Exception {
    SearchArgument sarg =
        SearchArgumentFactory.newBuilder()
            .startAnd()
            .lessThan("x", PredicateLeaf.Type.DATE,
                Date.valueOf("1970-1-11"))
            .lessThanEquals("y", PredicateLeaf.Type.STRING,
                new HiveChar("hi", 10).toString())
            .equals("z", PredicateLeaf.Type.DECIMAL, new HiveDecimalWritable("1.0"))
            .end()
            .build();
    assertEquals("leaf-0 = (LESS_THAN x 1970-01-11), " +
        "leaf-1 = (LESS_THAN_EQUALS y hi        ), " +
        "leaf-2 = (EQUALS z 1), " +
        "expr = (and leaf-0 leaf-1 leaf-2)", sarg.toString());

    sarg = SearchArgumentFactory.newBuilder()
        .startNot()
        .startOr()
        .isNull("x", PredicateLeaf.Type.LONG)
        .between("y", PredicateLeaf.Type.DECIMAL,
            new HiveDecimalWritable("10"), new HiveDecimalWritable("20.0"))
        .in("z", PredicateLeaf.Type.LONG, 1L, 2L, 3L)
        .nullSafeEquals("a", PredicateLeaf.Type.STRING,
            new HiveVarchar("stinger", 100).toString())
        .end()
        .end()
        .build();
    assertEquals("leaf-0 = (IS_NULL x), " +
        "leaf-1 = (BETWEEN y 10 20), " +
        "leaf-2 = (IN z 1 2 3), " +
        "leaf-3 = (NULL_SAFE_EQUALS a stinger), " +
        "expr = (and (not leaf-0) (not leaf-1) (not leaf-2) (not leaf-3))",
        sarg.toString());
  }

  @Test
  public void testBuilderComplexTypes2() throws Exception {
    SearchArgument sarg =
        SearchArgumentFactory.newBuilder()
            .startAnd()
            .lessThan("x", PredicateLeaf.Type.DATE, Date.valueOf("2005-3-12"))
            .lessThanEquals("y", PredicateLeaf.Type.STRING,
                new HiveChar("hi", 10).toString())
            .equals("z", PredicateLeaf.Type.DECIMAL,
                new HiveDecimalWritable("1.0"))
            .end()
            .build();
    assertEquals("leaf-0 = (LESS_THAN x 2005-03-12), " +
        "leaf-1 = (LESS_THAN_EQUALS y hi        ), " +
        "leaf-2 = (EQUALS z 1), " +
        "expr = (and leaf-0 leaf-1 leaf-2)", sarg.toString());

    sarg = SearchArgumentFactory.newBuilder()
        .startNot()
        .startOr()
        .isNull("x", PredicateLeaf.Type.LONG)
        .between("y", PredicateLeaf.Type.DECIMAL, new HiveDecimalWritable("10"),
            new HiveDecimalWritable("20.0"))
        .in("z", PredicateLeaf.Type.LONG, 1L, 2L, 3L)
        .nullSafeEquals("a", PredicateLeaf.Type.STRING,
            new HiveVarchar("stinger", 100).toString())
        .end()
        .end()
        .build();
    assertEquals("leaf-0 = (IS_NULL x), " +
        "leaf-1 = (BETWEEN y 10 20), " +
        "leaf-2 = (IN z 1 2 3), " +
        "leaf-3 = (NULL_SAFE_EQUALS a stinger), " +
        "expr = (and (not leaf-0) (not leaf-1) (not leaf-2) (not leaf-3))",
        sarg.toString());
  }

  @Test
  public void testBuilderFloat() throws Exception {
    SearchArgument sarg =
        SearchArgumentFactory.newBuilder()
            .startAnd()
            .lessThan("x", PredicateLeaf.Type.LONG, 22L)
            .lessThan("x1", PredicateLeaf.Type.LONG, 22L)
            .lessThanEquals("y", PredicateLeaf.Type.STRING,
                new HiveChar("hi", 10).toString())
            .equals("z", PredicateLeaf.Type.FLOAT, new Double(0.22))
            .equals("z1", PredicateLeaf.Type.FLOAT, new Double(0.22))
            .end()
            .build();
    assertEquals("leaf-0 = (LESS_THAN x 22), " +
        "leaf-1 = (LESS_THAN x1 22), " +
        "leaf-2 = (LESS_THAN_EQUALS y hi        ), " +
        "leaf-3 = (EQUALS z 0.22), " +
        "leaf-4 = (EQUALS z1 0.22), " +
        "expr = (and leaf-0 leaf-1 leaf-2 leaf-3 leaf-4)", sarg.toString());
  }

  @Test
  public void testTimestampSerialization() throws Exception {
    // There is a kryo which after serialize/deserialize,
    // Timestamp becomes Date. We get around this issue in
    // SearchArgumentImpl.getLiteral. Once kryo fixed the issue
    // We can simplify SearchArgumentImpl.getLiteral
    Timestamp now = new Timestamp(new java.util.Date().getTime());
    SearchArgument sarg =
      SearchArgumentFactory.newBuilder()
        .startAnd()
        .lessThan("x", PredicateLeaf.Type.TIMESTAMP, now)
        .end()
        .build();

    String serializedSarg = TestInputOutputFormat.toKryo(sarg);
    SearchArgument sarg2 = ConvertAstToSearchArg.create(serializedSarg);

    Field literalField = PredicateLeafImpl.class.getDeclaredField("literal");
    literalField.setAccessible(true);
    assertTrue(literalField.get(sarg2.getLeaves().get(0)) instanceof java.util.Date);
    Timestamp ts = (Timestamp)sarg2.getLeaves().get(0).getLiteral();
    assertEquals(ts, now);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testBadLiteral() throws Exception {
    SearchArgumentFactory.newBuilder()
        .startAnd()
        .lessThan("x", PredicateLeaf.Type.LONG, "hi")
        .end()
        .build();
  }

  @Test(expected = IllegalArgumentException.class)
  public void testBadLiteralList() throws Exception {
    SearchArgumentFactory.newBuilder()
            .startAnd()
            .in("x", PredicateLeaf.Type.STRING, "hi", 23)
            .end()
            .build();
  }
}
