/*
 * 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.phoenix.expression;

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

import java.util.Arrays;
import java.util.Collections;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellBuilderFactory;
import org.apache.hadoop.hbase.CellBuilderType;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.PBaseColumn;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PName;
import org.apache.phoenix.schema.PNameFactory;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.tuple.MultiKeyValueTuple;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PDataType;
import org.junit.Test;

public class OrExpressionTest {

  private OrExpression createOr(Expression lhs, Expression rhs) {
    return new OrExpression(Arrays.asList(lhs, rhs));
  }

  private OrExpression createOr(Boolean x, Boolean y) {
    return createOr(LiteralExpression.newConstant(x), LiteralExpression.newConstant(y));
  }

  private void testImmediateSingle(Boolean expected, Boolean lhs, Boolean rhs) {
    OrExpression or = createOr(lhs, rhs);
    ImmutableBytesWritable out = new ImmutableBytesWritable();
    MultiKeyValueTuple tuple = new MultiKeyValueTuple();
    boolean success = or.evaluate(tuple, out);
    assertTrue(success);
    assertEquals(expected, PBoolean.INSTANCE.toObject(out));
  }

  // Evaluating OR when values of both sides are known should immediately succeed
  // and return the same result regardless of order.
  private void testImmediate(Boolean expected, Boolean a, Boolean b) {
    testImmediateSingle(expected, a, b);
    testImmediateSingle(expected, b, a);
  }

  private PColumn pcolumn(final String name) {
    return new PBaseColumn() {
      @Override
      public PName getName() {
        return PNameFactory.newName(name);
      }

      @Override
      public PDataType getDataType() {
        return PBoolean.INSTANCE;
      }

      @Override
      public PName getFamilyName() {
        return PNameFactory.newName(QueryConstants.DEFAULT_COLUMN_FAMILY);
      }

      @Override
      public int getPosition() {
        return 0;
      }

      @Override
      public Integer getArraySize() {
        return null;
      }

      @Override
      public byte[] getViewConstant() {
        return new byte[0];
      }

      @Override
      public boolean isViewReferenced() {
        return false;
      }

      @Override
      public String getExpressionStr() {
        return null;
      }

      @Override
      public boolean isRowTimestamp() {
        return false;
      }

      @Override
      public boolean isDynamic() {
        return false;
      }

      @Override
      public byte[] getColumnQualifierBytes() {
        return null;
      }

      @Override
      public long getTimestamp() {
        return 0;
      }

      @Override
      public boolean isDerived() {
        return false;
      }

      @Override
      public boolean isExcluded() {
        return false;
      }

      @Override
      public SortOrder getSortOrder() {
        return null;
      }
    };
  }

  private KeyValueColumnExpression kvExpr(final String name) {
    return new KeyValueColumnExpression(pcolumn(name));
  }

  private Cell createCell(String name, Boolean value) {
    byte[] valueBytes = value == null ? null : value ? PBoolean.TRUE_BYTES : PBoolean.FALSE_BYTES;
    return CellBuilderFactory.create(CellBuilderType.DEEP_COPY).setRow(Bytes.toBytes("row"))
      .setFamily(QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES).setQualifier(Bytes.toBytes(name))
      .setTimestamp(1).setType(Cell.Type.Put).setValue(valueBytes).build();
  }

  private void testPartialOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs,
    boolean leftFirst) {
    KeyValueColumnExpression lhsExpr = kvExpr("LHS");
    KeyValueColumnExpression rhsExpr = kvExpr("RHS");
    OrExpression or = createOr(lhsExpr, rhsExpr);
    MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell> emptyList());
    ImmutableBytesWritable out = new ImmutableBytesWritable();

    // with no data available, should fail
    boolean success = or.evaluate(tuple, out);
    assertFalse(success);

    // with 1 datum available, should fail
    if (leftFirst) {
      tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs)));
    } else {
      tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs)));
    }
    success = or.evaluate(tuple, out);
    assertFalse(success);

    // with 2 data available, should succeed
    tuple.setKeyValues(Arrays.asList(createCell("LHS", lhs), createCell("RHS", rhs)));
    success = or.evaluate(tuple, out);
    assertTrue(success);
    assertEquals(expected, PBoolean.INSTANCE.toObject(out));
  }

  private void testPartialEvaluation(Boolean expected, Boolean x, Boolean y, boolean xFirst) {
    testPartialOneSideFirst(expected, x, y, xFirst);
    testPartialOneSideFirst(expected, y, x, !xFirst);
  }

  private void testShortCircuitOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs,
    boolean leftFirst) {
    KeyValueColumnExpression lhsExpr = kvExpr("LHS");
    KeyValueColumnExpression rhsExpr = kvExpr("RHS");
    OrExpression or = createOr(lhsExpr, rhsExpr);
    MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell> emptyList());
    ImmutableBytesWritable out = new ImmutableBytesWritable();

    // with no data available, should fail
    boolean success = or.evaluate(tuple, out);
    assertFalse(success);

    // with 1 datum available, should succeed
    if (leftFirst) {
      tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs)));
    } else {
      tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs)));
    }
    success = or.evaluate(tuple, out);
    assertTrue(success);
    assertEquals(expected, PBoolean.INSTANCE.toObject(out));
  }

  private void testShortCircuit(Boolean expected, Boolean x, Boolean y, boolean xFirst) {
    testShortCircuitOneSideFirst(expected, x, y, xFirst);
    testShortCircuitOneSideFirst(expected, y, x, !xFirst);
  }

  @Test
  public void testImmediateCertainty() {
    testImmediate(true, true, true);
    testImmediate(true, false, true);
    testImmediate(false, false, false);
  }

  @Test
  public void testImmediateUncertainty() {
    testImmediate(true, true, null);
    testImmediate(null, false, null);
    testImmediate(null, null, null);
  }

  @Test
  public void testPartialCertainty() {
    // must evaluate both sides if FALSE is evaluated first

    // F OR F = F
    testPartialEvaluation(false, false, false, true);
    testPartialEvaluation(false, false, false, false);

    // T OR F = T
    testPartialEvaluation(true, false, true, true);
    testPartialEvaluation(true, true, false, false);
  }

  @Test
  public void testPartialUncertainty() {
    // T OR NULL = NULL
    testPartialEvaluation(true, null, true, true);
    testPartialEvaluation(true, true, null, false);

    // must evaluate both sides if NULL is evaluated first

    // F OR NULL = NULL
    testPartialEvaluation(null, null, false, true);
    testPartialEvaluation(null, false, null, false);

    // NULL OR NULL = NULL
    testPartialEvaluation(null, null, null, true);
    testPartialEvaluation(null, null, null, false);
  }

  @Test
  public void testShortCircuitCertainty() {
    // need only to evaluate one side if TRUE is evaluated first

    // T OR T = T
    testShortCircuit(true, true, true, true);
    testShortCircuit(true, true, true, false);

    // T OR F = F
    testShortCircuit(true, true, false, true);
    testShortCircuit(true, false, true, false);
  }

  @Test
  public void testShortCircuitUncertainty() {
    // need only to evaluate one side if TRUE is evaluated first
    testShortCircuit(true, true, null, true);
    testShortCircuit(true, null, true, false);
  }

  @Test
  public void testTruthTable() {
    // See:
    // https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL)
    Boolean[][] testCases = new Boolean[][] {
      // should short circuit?
      // X, Y, if X first, if Y first, X OR Y,
      { true, true, true, true, true, }, { true, false, true, false, true, },
      { false, false, false, false, false, }, { true, null, true, false, true, },
      { false, null, false, false, null, }, { null, null, false, false, null, }, };

    for (Boolean[] testCase : testCases) {
      Boolean x = testCase[0];
      Boolean y = testCase[1];
      boolean shouldShortCircuitWhenXEvaluatedFirst = testCase[2];
      boolean shouldShortCircuitWhenYEvaluatedFirst = testCase[3];
      Boolean expected = testCase[4];

      // test both directions
      testImmediate(expected, x, y);

      if (shouldShortCircuitWhenXEvaluatedFirst) {
        testShortCircuit(expected, x, y, true);
      } else {
        testPartialEvaluation(expected, x, y, true);
      }

      if (shouldShortCircuitWhenYEvaluatedFirst) {
        testShortCircuit(expected, x, y, false);
      } else {
        testPartialEvaluation(expected, x, y, false);
      }
    }
  }
}
