/*
 * 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.hbase.filter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellComparator;
import org.apache.hadoop.hbase.CompareOperator;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.KeyValueUtil;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.filter.Filter.ReturnCode;
import org.apache.hadoop.hbase.filter.FilterList.Operator;
import org.apache.hadoop.hbase.testclassification.FilterTests;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import org.apache.hbase.thirdparty.com.google.common.collect.Lists;

import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;

@Tag(FilterTests.TAG)
@Tag(MediumTests.TAG)
public class TestFilterList {

  static final int MAX_PAGES = 2;

  @Test
  public void testAddFilter() throws Exception {
    Filter filter1 = new FirstKeyOnlyFilter();
    Filter filter2 = new FirstKeyOnlyFilter();

    FilterList filterList = new FilterList(filter1, filter2);
    filterList.addFilter(new FirstKeyOnlyFilter());

    filterList = new FilterList(Arrays.asList(filter1, filter2));
    filterList.addFilter(new FirstKeyOnlyFilter());

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter1, filter2);
    filterList.addFilter(new FirstKeyOnlyFilter());

    filterList = new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(filter1, filter2));
    filterList.addFilter(new FirstKeyOnlyFilter());

    filterList.setReversed(false);
    FirstKeyOnlyFilter f = new FirstKeyOnlyFilter();
    f.setReversed(true);
    try {
      filterList.addFilter(f);
      fail("The IllegalArgumentException should be thrown because the added filter is reversed");
    } catch (IllegalArgumentException e) {
    }

  }

  @Test
  public void testConstruction() {
    FirstKeyOnlyFilter f1 = new FirstKeyOnlyFilter();
    FirstKeyOnlyFilter f2 = new FirstKeyOnlyFilter();
    f1.setReversed(true);
    f2.setReversed(false);

    try {
      FilterList ff = new FilterList(f1, f2);
      fail("The IllegalArgumentException should be thrown");
    } catch (IllegalArgumentException e) {
    }

    try {
      FilterList ff = new FilterList(Arrays.asList(f1, f2));
      fail("The IllegalArgumentException should be thrown because the added filter is reversed");
    } catch (IllegalArgumentException e) {
    }

    try {
      FilterList ff = new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(f1, f2));
      fail("The IllegalArgumentException should be thrown because the added filter is reversed");
    } catch (IllegalArgumentException e) {
    }

    try {
      FilterList ff = new FilterList(FilterList.Operator.MUST_PASS_ALL, f1, f2);
      fail("The IllegalArgumentException should be thrown because the added filter is reversed");
    } catch (IllegalArgumentException e) {
    }
  }

  /**
   * Test "must pass one"
   */
  @Test
  public void testMPONE() throws Exception {
    mpOneTest(getFilterMPONE());
  }

  private Filter getFilterMPONE() {
    List<Filter> filters = new ArrayList<>();
    filters.add(new PageFilter(MAX_PAGES));
    filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
    Filter filterMPONE = new FilterList(FilterList.Operator.MUST_PASS_ONE, filters);
    return filterMPONE;
  }

  private void mpOneTest(Filter filterMPONE) throws Exception {
    /*
     * Filter must do all below steps: <ul> <li>{@link #reset()}</li> <li>{@link
     * #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li> <li>{@link
     * #filterRowKey(byte[],int,int)} -> true to drop this row, if false, we will also call</li>
     * <li>{@link #filterCell(org.apache.hadoop.hbase.KeyValue)} -> true to drop this cell</li>
     * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
     * filterValue() calls. Eg: filter a row if it doesn't contain a specified column. </li> </ul>
     */
    filterMPONE.reset();
    assertFalse(filterMPONE.filterAllRemaining());

    /* Will pass both */
    byte[] rowkey = Bytes.toBytes("yyyyyyyyy");
    for (int i = 0; i < MAX_PAGES - 1; i++) {
      assertFalse(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
      KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i), Bytes.toBytes(i));
      assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
      assertFalse(filterMPONE.filterRow());
    }

    /* Only pass PageFilter */
    rowkey = Bytes.toBytes("z");
    assertFalse(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
    KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(0), Bytes.toBytes(0));
    assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
    assertFalse(filterMPONE.filterRow());

    /* reach MAX_PAGES already, should filter any rows */
    rowkey = Bytes.toBytes("yyy");
    assertTrue(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
    kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(0), Bytes.toBytes(0));
    assertFalse(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
    // FilterList.filterRow() returns true because previously "z" was filtered out (return true) by
    // PrefixFilter.filterRowKey()
    assertTrue(filterMPONE.filterRow());

    /* We should filter any row */
    rowkey = Bytes.toBytes("z");
    assertTrue(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
    assertTrue(filterMPONE.filterAllRemaining());
  }

  /**
   * Test "must pass all"
   */
  @Test
  public void testMPALL() throws Exception {
    mpAllTest(getMPALLFilter());
  }

  private Filter getMPALLFilter() {
    List<Filter> filters = new ArrayList<>();
    filters.add(new PageFilter(MAX_PAGES));
    filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
    Filter filterMPALL = new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);
    return filterMPALL;
  }

  private void mpAllTest(Filter filterMPALL) throws Exception {
    /*
     * Filter must do all below steps: <ul> <li>{@link #reset()}</li> <li>{@link
     * #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li> <li>{@link
     * #filterRowKey(byte[],int,int)} -> true to drop this row, if false, we will also call</li>
     * <li>{@link #filterCell(org.apache.hadoop.hbase.KeyValue)} -> true to drop this cell</li>
     * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
     * filterValue() calls. Eg: filter a row if it doesn't contain a specified column. </li> </ul>
     */
    filterMPALL.reset();
    assertFalse(filterMPALL.filterAllRemaining());
    byte[] rowkey = Bytes.toBytes("yyyyyyyyy");
    for (int i = 0; i < MAX_PAGES - 1; i++) {
      assertFalse(filterMPALL.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
      KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i), Bytes.toBytes(i));
      assertTrue(Filter.ReturnCode.INCLUDE == filterMPALL.filterCell(kv));
    }
    filterMPALL.reset();
    rowkey = Bytes.toBytes("z");
    assertTrue(filterMPALL.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
    // Should fail here; row should be filtered out.
    KeyValue kv = new KeyValue(rowkey, rowkey, rowkey, rowkey);
    assertTrue(Filter.ReturnCode.NEXT_ROW == filterMPALL.filterCell(kv));
  }

  /**
   * Test list ordering
   */
  @Test
  public void testOrdering() throws Exception {
    orderingTest(getOrderingFilter());
  }

  public Filter getOrderingFilter() {
    List<Filter> filters = new ArrayList<>();
    filters.add(new PrefixFilter(Bytes.toBytes("yyy")));
    filters.add(new PageFilter(MAX_PAGES));
    Filter filterMPONE = new FilterList(FilterList.Operator.MUST_PASS_ONE, filters);
    return filterMPONE;
  }

  public void orderingTest(Filter filterMPONE) throws Exception {
    /*
     * Filter must do all below steps: <ul> <li>{@link #reset()}</li> <li>{@link
     * #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li> <li>{@link
     * #filterRowKey(byte[],int,int)} -> true to drop this row, if false, we will also call</li>
     * <li>{@link #filterCell(org.apache.hadoop.hbase.KeyValue)} -> true to drop this key/value</li>
     * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
     * filterValue() calls. Eg: filter a row if it doesn't contain a specified column. </li> </ul>
     */
    filterMPONE.reset();
    assertFalse(filterMPONE.filterAllRemaining());

    /* We should be able to fill MAX_PAGES without incrementing page counter */
    byte[] rowkey = Bytes.toBytes("yyyyyyyy");
    for (int i = 0; i < MAX_PAGES; i++) {
      assertFalse(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
      KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i), Bytes.toBytes(i));
      assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
      assertFalse(filterMPONE.filterRow());
    }

    /* Now let's fill the page filter */
    rowkey = Bytes.toBytes("xxxxxxx");
    for (int i = 0; i < MAX_PAGES; i++) {
      assertFalse(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
      KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i), Bytes.toBytes(i));
      assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
      assertFalse(filterMPONE.filterRow());
    }

    /* We should still be able to include even though page filter is at max */
    rowkey = Bytes.toBytes("yyy");
    for (int i = 0; i < MAX_PAGES; i++) {
      assertFalse(filterMPONE.filterRowKey(KeyValueUtil.createFirstOnRow(rowkey)));
      KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i), Bytes.toBytes(i));
      assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterCell(kv));
      assertFalse(filterMPONE.filterRow());
    }
  }

  /**
   * When we do a "MUST_PASS_ONE" (a logical 'OR') of the above two filters we expect to get the
   * same result as the 'prefix' only result.
   */
  @Test
  public void testFilterListTwoFiltersMustPassOne() throws Exception {
    byte[] r1 = Bytes.toBytes("Row1");
    byte[] r11 = Bytes.toBytes("Row11");
    byte[] r2 = Bytes.toBytes("Row2");

    FilterList flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
    flist.addFilter(new PrefixFilter(r1));
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r1));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r1, r1, r1)));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r11, r11, r11)));

    flist.reset();
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r2));
    assertEquals(ReturnCode.SKIP, flist.filterCell(new KeyValue(r2, r2, r2)));

    flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
    flist.addFilter(new AlwaysNextColFilter());
    flist.addFilter(new PrefixFilter(r1));
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r1));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r1, r1, r1)));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r11, r11, r11)));

    flist.reset();
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r2));
    assertEquals(ReturnCode.NEXT_COL, flist.filterCell(new KeyValue(r2, r2, r2)));
  }

  /**
   * When we do a "MUST_PASS_ONE" (a logical 'OR') of the two filters we expect to get the same
   * result as the inclusive stop result.
   */
  @Test
  public void testFilterListWithInclusiveStopFilterMustPassOne() throws Exception {
    byte[] r1 = Bytes.toBytes("Row1");
    byte[] r11 = Bytes.toBytes("Row11");
    byte[] r2 = Bytes.toBytes("Row2");

    FilterList flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
    flist.addFilter(new AlwaysNextColFilter());
    flist.addFilter(new InclusiveStopFilter(r1));
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r1));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r1, r1, r1)));
    assertEquals(ReturnCode.INCLUDE, flist.filterCell(new KeyValue(r11, r11, r11)));

    flist.reset();
    flist.filterRowKey(KeyValueUtil.createFirstOnRow(r2));
    assertEquals(ReturnCode.NEXT_COL, flist.filterCell(new KeyValue(r2, r2, r2)));
  }

  public static class AlwaysNextColFilter extends FilterBase {
    public AlwaysNextColFilter() {
      super();
    }

    @Override
    public ReturnCode filterCell(final Cell v) {
      return ReturnCode.NEXT_COL;
    }

    public static AlwaysNextColFilter parseFrom(final byte[] pbBytes)
      throws DeserializationException {
      return new AlwaysNextColFilter();
    }
  }

  /**
   * Test serialization
   */
  @Test
  public void testSerialization() throws Exception {
    List<Filter> filters = new ArrayList<>();
    filters.add(new PageFilter(MAX_PAGES));
    filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
    Filter filterMPALL = new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);

    // Decompose filterMPALL to bytes.
    byte[] buffer = filterMPALL.toByteArray();

    // Recompose filterMPALL.
    FilterList newFilter = FilterList.parseFrom(buffer);

    // Run tests
    mpOneTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getFilterMPONE())));
    mpAllTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getMPALLFilter())));
    orderingTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getOrderingFilter())));
  }

  /**
   * Test filterCell logic.
   */
  @Test
  public void testFilterCell() throws Exception {
    Filter includeFilter = new FilterBase() {
      @Override
      public Filter.ReturnCode filterCell(final Cell v) {
        return Filter.ReturnCode.INCLUDE;
      }
    };

    Filter alternateFilter = new FilterBase() {
      boolean returnInclude = true;

      @Override
      public Filter.ReturnCode filterCell(final Cell v) {
        Filter.ReturnCode returnCode =
          returnInclude ? Filter.ReturnCode.INCLUDE : Filter.ReturnCode.SKIP;
        returnInclude = !returnInclude;
        return returnCode;
      }
    };

    Filter alternateIncludeFilter = new FilterBase() {
      boolean returnIncludeOnly = false;

      @Override
      public Filter.ReturnCode filterCell(final Cell v) {
        Filter.ReturnCode returnCode =
          returnIncludeOnly ? Filter.ReturnCode.INCLUDE : Filter.ReturnCode.INCLUDE_AND_NEXT_COL;
        returnIncludeOnly = !returnIncludeOnly;
        return returnCode;
      }
    };

    // Check must pass one filter.
    FilterList mpOnefilterList = new FilterList(Operator.MUST_PASS_ONE,
      Arrays.asList(new Filter[] { includeFilter, alternateIncludeFilter, alternateFilter }));
    // INCLUDE, INCLUDE, INCLUDE_AND_NEXT_COL.
    assertEquals(ReturnCode.INCLUDE, mpOnefilterList.filterCell(null));
    // INCLUDE, SKIP, INCLUDE.
    assertEquals(Filter.ReturnCode.INCLUDE, mpOnefilterList.filterCell(null));

    // Check must pass all filter.
    FilterList mpAllfilterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { includeFilter, alternateIncludeFilter, alternateFilter }));
    // INCLUDE, INCLUDE, INCLUDE_AND_NEXT_COL.
    assertEquals(Filter.ReturnCode.INCLUDE_AND_NEXT_COL, mpAllfilterList.filterCell(null));
    // INCLUDE, SKIP, INCLUDE.
    assertEquals(Filter.ReturnCode.SKIP, mpAllfilterList.filterCell(null));
  }

  /**
   * Test pass-thru of hints.
   */
  @Test
  public void testHintPassThru() throws Exception {

    final KeyValue minKeyValue = new KeyValue(Bytes.toBytes(0L), null, null);
    final KeyValue maxKeyValue = new KeyValue(Bytes.toBytes(Long.MAX_VALUE), null, null);

    Filter filterNoHint = new FilterBase() {
      @Override
      public byte[] toByteArray() {
        return null;
      }

      @Override
      public ReturnCode filterCell(final Cell ignored) throws IOException {
        return ReturnCode.INCLUDE;
      }
    };

    Filter filterMinHint = new FilterBase() {
      @Override
      public ReturnCode filterCell(final Cell ignored) {
        return ReturnCode.SEEK_NEXT_USING_HINT;
      }

      @Override
      public Cell getNextCellHint(Cell currentKV) {
        return minKeyValue;
      }

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

    Filter filterMaxHint = new FilterBase() {
      @Override
      public ReturnCode filterCell(final Cell ignored) {
        return ReturnCode.SEEK_NEXT_USING_HINT;
      }

      @Override
      public Cell getNextCellHint(Cell cell) {
        return new KeyValue(Bytes.toBytes(Long.MAX_VALUE), null, null);
      }

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

    CellComparator comparator = CellComparator.getInstance();
    // MUST PASS ONE

    // Should take the min if given two hints
    FilterList filterList = new FilterList(Operator.MUST_PASS_ONE,
      Arrays.asList(new Filter[] { filterMinHint, filterMaxHint }));
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), minKeyValue));

    // Should have no hint if any filter has no hint
    filterList = new FilterList(Operator.MUST_PASS_ONE,
      Arrays.asList(new Filter[] { filterMinHint, filterMaxHint, filterNoHint }));
    assertNull(filterList.getNextCellHint(null));
    filterList = new FilterList(Operator.MUST_PASS_ONE,
      Arrays.asList(new Filter[] { filterNoHint, filterMaxHint }));
    assertNull(filterList.getNextCellHint(null));

    // Should give max hint if its the only one
    filterList = new FilterList(Operator.MUST_PASS_ONE,
      Arrays.asList(new Filter[] { filterMaxHint, filterMaxHint }));
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), maxKeyValue));

    // MUST PASS ALL

    // Should take the first hint
    filterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { filterMinHint, filterMaxHint }));
    filterList.filterCell(null);
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), minKeyValue));

    filterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { filterMaxHint, filterMinHint }));
    filterList.filterCell(null);
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), maxKeyValue));

    // Should have first hint even if a filter has no hint
    filterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { filterNoHint, filterMinHint, filterMaxHint }));
    filterList.filterCell(null);
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), minKeyValue));
    filterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { filterNoHint, filterMaxHint }));
    filterList.filterCell(null);
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), maxKeyValue));
    filterList = new FilterList(Operator.MUST_PASS_ALL,
      Arrays.asList(new Filter[] { filterNoHint, filterMinHint }));
    filterList.filterCell(null);
    assertEquals(0, comparator.compare(filterList.getNextCellHint(null), minKeyValue));
  }

  /**
   * Tests the behavior of transform() in a hierarchical filter. transform() only applies after a
   * filterCell() whose return-code includes the KeyValue. Lazy evaluation of AND
   */
  @Test
  public void testTransformMPO() throws Exception {
    // Apply the following filter:
    // (family=fam AND qualifier=qual1 AND KeyOnlyFilter)
    // OR (family=fam AND qualifier=qual2)
    final FilterList flist = new FilterList(Operator.MUST_PASS_ONE,
      Lists.<Filter> newArrayList(
        new FilterList(Operator.MUST_PASS_ALL,
          Lists.<Filter> newArrayList(
            new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("fam"))),
            new QualifierFilter(CompareOperator.EQUAL,
              new BinaryComparator(Bytes.toBytes("qual1"))),
            new KeyOnlyFilter())),
        new FilterList(Operator.MUST_PASS_ALL,
          Lists.<Filter> newArrayList(
            new FamilyFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("fam"))),
            new QualifierFilter(CompareOperator.EQUAL,
              new BinaryComparator(Bytes.toBytes("qual2")))))));

    final KeyValue kvQual1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"),
      Bytes.toBytes("qual1"), Bytes.toBytes("value"));
    final KeyValue kvQual2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"),
      Bytes.toBytes("qual2"), Bytes.toBytes("value"));
    final KeyValue kvQual3 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"),
      Bytes.toBytes("qual3"), Bytes.toBytes("value"));

    // Value for fam:qual1 should be stripped:
    assertEquals(Filter.ReturnCode.INCLUDE, flist.filterCell(kvQual1));
    final KeyValue transformedQual1 = KeyValueUtil.ensureKeyValue(flist.transformCell(kvQual1));
    assertEquals(0, transformedQual1.getValueLength());

    // Value for fam:qual2 should not be stripped:
    assertEquals(Filter.ReturnCode.INCLUDE, flist.filterCell(kvQual2));
    final KeyValue transformedQual2 = KeyValueUtil.ensureKeyValue(flist.transformCell(kvQual2));
    assertEquals("value", Bytes.toString(transformedQual2.getValueArray(),
      transformedQual2.getValueOffset(), transformedQual2.getValueLength()));

    // Other keys should be skipped:
    assertEquals(Filter.ReturnCode.SKIP, flist.filterCell(kvQual3));
  }

  @Test
  public void testWithMultiVersionsInSameRow() throws Exception {
    FilterList filterList01 =
      new FilterList(Operator.MUST_PASS_ONE, new ColumnPaginationFilter(1, 0));

    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual"),
      1, Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual"),
      2, Bytes.toBytes("value"));
    KeyValue kv3 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual"),
      3, Bytes.toBytes("value"));

    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList01.filterCell(kv1));
    assertEquals(ReturnCode.NEXT_COL, filterList01.filterCell(kv2));
    assertEquals(ReturnCode.NEXT_COL, filterList01.filterCell(kv3));

    FilterList filterList11 =
      new FilterList(Operator.MUST_PASS_ONE, new ColumnPaginationFilter(1, 1));

    assertEquals(ReturnCode.NEXT_COL, filterList11.filterCell(kv1));
    assertEquals(ReturnCode.NEXT_COL, filterList11.filterCell(kv2));
    assertEquals(ReturnCode.NEXT_COL, filterList11.filterCell(kv3));
  }

  @Test
  public void testMPONEWithSeekNextUsingHint() throws Exception {
    byte[] col = Bytes.toBytes("c");
    FilterList filterList =
      new FilterList(Operator.MUST_PASS_ONE, new ColumnPaginationFilter(1, col));

    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("b"), 2,
      Bytes.toBytes("value"));
    KeyValue kv3 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("c"), 3,
      Bytes.toBytes("value"));
    KeyValue kv4 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("c"), 4,
      Bytes.toBytes("value"));

    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv2));
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv3));
    assertEquals(ReturnCode.NEXT_COL, filterList.filterCell(kv4));
  }

  private static class MockFilter extends FilterBase {
    private ReturnCode targetRetCode;
    public boolean didCellPassToTheFilter = false;

    public MockFilter(ReturnCode targetRetCode) {
      this.targetRetCode = targetRetCode;
    }

    @Override
    public ReturnCode filterCell(final Cell v) throws IOException {
      this.didCellPassToTheFilter = true;
      return targetRetCode;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == null || !(obj instanceof MockFilter)) {
        return false;
      }
      if (obj == this) {
        return true;
      }
      MockFilter f = (MockFilter) obj;
      return this.targetRetCode.equals(f.targetRetCode);
    }

    @Override
    public int hashCode() {
      return Objects.hash(this.targetRetCode);
    }
  }

  private static class HintingMockFilter extends MockFilter implements HintingFilter {
    public HintingMockFilter(ReturnCode targetRetCode) {
      super(targetRetCode);
    }
  }

  @Test
  public void testShouldPassCurrentCellToFilter() throws IOException {
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 2,
      Bytes.toBytes("value"));
    KeyValue kv3 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("b"), 3,
      Bytes.toBytes("value"));
    KeyValue kv4 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fam"), Bytes.toBytes("c"), 4,
      Bytes.toBytes("value"));

    MockFilter mockFilter = new MockFilter(ReturnCode.NEXT_COL);
    FilterList filter = new FilterList(Operator.MUST_PASS_ONE, mockFilter);

    filter.filterCell(kv1);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv2);
    assertFalse(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv3);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter = new MockFilter(ReturnCode.INCLUDE_AND_NEXT_COL);
    filter = new FilterList(Operator.MUST_PASS_ONE, mockFilter);

    filter.filterCell(kv1);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv2);
    assertFalse(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv3);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter = new MockFilter(ReturnCode.NEXT_ROW);
    filter = new FilterList(Operator.MUST_PASS_ONE, mockFilter);
    filter.filterCell(kv1);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv2);
    assertFalse(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv3);
    assertFalse(mockFilter.didCellPassToTheFilter);

    filter.reset();
    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv4);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter = new MockFilter(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW);
    filter = new FilterList(Operator.MUST_PASS_ONE, mockFilter);
    filter.filterCell(kv1);
    assertTrue(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv2);
    assertFalse(mockFilter.didCellPassToTheFilter);

    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv3);
    assertFalse(mockFilter.didCellPassToTheFilter);

    filter.reset();
    mockFilter.didCellPassToTheFilter = false;
    filter.filterCell(kv4);
    assertTrue(mockFilter.didCellPassToTheFilter);
  }

  @Test
  public void testTheMaximalRule() throws IOException {
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    MockFilter filter1 = new MockFilter(ReturnCode.INCLUDE);
    MockFilter filter2 = new MockFilter(ReturnCode.INCLUDE_AND_NEXT_COL);
    MockFilter filter3 = new MockFilter(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW);
    MockFilter filter4 = new MockFilter(ReturnCode.NEXT_COL);
    MockFilter filter5 = new MockFilter(ReturnCode.SKIP);
    MockFilter filter6 = new HintingMockFilter(ReturnCode.SEEK_NEXT_USING_HINT);
    MockFilter filter7 = new MockFilter(ReturnCode.NEXT_ROW);

    FilterList filterList = new FilterList(Operator.MUST_PASS_ALL, filter1, filter2);
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter2, filter3);
    assertEquals(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter4, filter5, filter6);
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter4, filter6);
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter3, filter1);
    assertEquals(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter3, filter2, filter1, filter5);
    assertEquals(ReturnCode.NEXT_ROW, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter2,
      new FilterList(Operator.MUST_PASS_ALL, filter3, filter4));
    assertEquals(ReturnCode.NEXT_ROW, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL, filter3, filter7);
    assertEquals(ReturnCode.NEXT_ROW, filterList.filterCell(kv1));
  }

  @Test
  public void testTheMinimalRule() throws IOException {
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    MockFilter filter1 = new MockFilter(ReturnCode.INCLUDE);
    MockFilter filter2 = new MockFilter(ReturnCode.INCLUDE_AND_NEXT_COL);
    MockFilter filter3 = new MockFilter(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW);
    MockFilter filter4 = new MockFilter(ReturnCode.NEXT_COL);
    MockFilter filter5 = new MockFilter(ReturnCode.SKIP);
    MockFilter filter6 = new HintingMockFilter(ReturnCode.SEEK_NEXT_USING_HINT);
    FilterList filterList = new FilterList(Operator.MUST_PASS_ONE, filter1, filter2);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter2, filter3);
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter4, filter5, filter6);
    assertEquals(ReturnCode.SKIP, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter4, filter6);
    assertEquals(ReturnCode.SKIP, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter3, filter1);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter3, filter2, filter1, filter5);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter2,
      new FilterList(Operator.MUST_PASS_ONE, filter3, filter4));
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter2,
      new FilterList(Operator.MUST_PASS_ONE, filter3, filter4));
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ONE, filter6, filter6);
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));
  }

  static class MockSeekHintFilter extends FilterBase {
    private Cell returnCell;

    public MockSeekHintFilter(Cell returnCell) {
      this.returnCell = returnCell;
    }

    @Override
    public ReturnCode filterCell(final Cell v) throws IOException {
      return ReturnCode.SEEK_NEXT_USING_HINT;
    }

    @Override
    public Cell getNextCellHint(Cell currentCell) throws IOException {
      return this.returnCell;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == null || !(obj instanceof MockSeekHintFilter)) {
        return false;
      }
      if (obj == this) {
        return true;
      }
      MockSeekHintFilter f = (MockSeekHintFilter) obj;
      return this.returnCell.equals(f.returnCell);
    }

    @Override
    public int hashCode() {
      return Objects.hash(this.returnCell);
    }
  }

  @Test
  public void testReversedFilterListWithMockSeekHintFilter() throws IOException {
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row2"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    KeyValue kv3 = new KeyValue(Bytes.toBytes("row3"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    Filter filter1 = new MockSeekHintFilter(kv1);
    filter1.setReversed(true);
    Filter filter2 = new MockSeekHintFilter(kv2);
    filter2.setReversed(true);
    Filter filter3 = new MockSeekHintFilter(kv3);
    filter3.setReversed(true);

    FilterList filterList = new FilterList(Operator.MUST_PASS_ONE);
    filterList.setReversed(true);
    filterList.addFilter(filter1);
    filterList.addFilter(filter2);
    filterList.addFilter(filter3);

    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));
    assertEquals(kv3, filterList.getNextCellHint(kv1));

    filterList = new FilterList(Operator.MUST_PASS_ALL);
    filterList.setReversed(true);
    filterList.addFilter(filter1);
    filterList.addFilter(filter2);
    filterList.addFilter(filter3);

    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));
    assertEquals(kv1, filterList.getNextCellHint(kv1));
  }

  @Test
  public void testReversedFilterListWithOR() throws IOException {
    byte[] r22 = Bytes.toBytes("Row22");
    byte[] r2 = Bytes.toBytes("Row2");
    byte[] r1 = Bytes.toBytes("Row1");

    FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ONE);
    filterList.setReversed(true);
    PrefixFilter prefixFilter = new PrefixFilter(r2);
    prefixFilter.setReversed(true);
    filterList.addFilter(prefixFilter);
    filterList.filterRowKey(KeyValueUtil.createFirstOnRow(r22));
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(new KeyValue(r22, r22, r22)));
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(new KeyValue(r2, r2, r2)));

    filterList.reset();
    filterList.filterRowKey(KeyValueUtil.createFirstOnRow(r1));
    assertEquals(ReturnCode.SKIP, filterList.filterCell(new KeyValue(r1, r1, r1)));

    filterList = new FilterList(FilterList.Operator.MUST_PASS_ONE);
    filterList.setReversed(true);
    AlwaysNextColFilter alwaysNextColFilter = new AlwaysNextColFilter();
    alwaysNextColFilter.setReversed(true);
    prefixFilter = new PrefixFilter(r2);
    prefixFilter.setReversed(true);
    filterList.addFilter(alwaysNextColFilter);
    filterList.addFilter(prefixFilter);
    filterList.filterRowKey(KeyValueUtil.createFirstOnRow(r22));
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(new KeyValue(r22, r22, r22)));
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(new KeyValue(r2, r2, r2)));

    filterList.reset();
    filterList.filterRowKey(KeyValueUtil.createFirstOnRow(r1));
    assertEquals(ReturnCode.NEXT_COL, filterList.filterCell(new KeyValue(r1, r1, r1)));
  }

  @Test
  public void testKeyOnlyFilterTransformCell() throws IOException {
    Cell c;
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      1, Bytes.toBytes("value1"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      2, Bytes.toBytes("value2"));

    Filter filter1 = new SingleColumnValueFilter(Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      CompareOperator.EQUAL, Bytes.toBytes("value1"));
    Filter filter2 = new SingleColumnValueFilter(Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      CompareOperator.EQUAL, Bytes.toBytes("value2"));
    FilterList internalFilterList = new FilterList(Operator.MUST_PASS_ONE, filter1, filter2);

    FilterList keyOnlyFilterFirst =
      new FilterList(Operator.MUST_PASS_ALL, new KeyOnlyFilter(), internalFilterList);

    assertEquals(ReturnCode.INCLUDE, keyOnlyFilterFirst.filterCell(kv1));
    c = keyOnlyFilterFirst.transformCell(kv1);
    assertEquals(0, c.getValueLength());
    assertEquals(ReturnCode.INCLUDE, keyOnlyFilterFirst.filterCell(kv2));
    c = keyOnlyFilterFirst.transformCell(kv2);
    assertEquals(0, c.getValueLength());

    internalFilterList.reset();
    FilterList keyOnlyFilterLast =
      new FilterList(Operator.MUST_PASS_ALL, new KeyOnlyFilter(), internalFilterList);
    assertEquals(ReturnCode.INCLUDE, keyOnlyFilterLast.filterCell(kv1));
    c = keyOnlyFilterLast.transformCell(kv1);
    assertEquals(0, c.getValueLength());
    assertEquals(ReturnCode.INCLUDE, keyOnlyFilterLast.filterCell(kv2));
    c = keyOnlyFilterLast.transformCell(kv2);
    assertEquals(0, c.getValueLength());
  }

  @Test
  public void testEmptyFilterListTransformCell() throws IOException {
    KeyValue kv = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      1, Bytes.toBytes("value"));
    FilterList filterList = new FilterList(Operator.MUST_PASS_ALL);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv));
    assertEquals(kv, filterList.transformCell(kv));

    filterList = new FilterList(Operator.MUST_PASS_ONE);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv));
    assertEquals(kv, filterList.transformCell(kv));
  }

  private static class MockNextRowFilter extends FilterBase {
    private int hitCount = 0;

    @Override
    public ReturnCode filterCell(final Cell v) throws IOException {
      hitCount++;
      return ReturnCode.NEXT_ROW;
    }

    public int getHitCount() {
      return hitCount;
    }
  }

  @Test
  public void testRowCountFilter() throws IOException {
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam1"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("fam2"), Bytes.toBytes("a"), 2,
      Bytes.toBytes("value"));
    MockNextRowFilter mockNextRowFilter = new MockNextRowFilter();
    FilterList filter = new FilterList(Operator.MUST_PASS_ONE, mockNextRowFilter);
    filter.filterCell(kv1);
    filter.filterCell(kv2);
    assertEquals(2, mockNextRowFilter.getHitCount());
  }

  private static class TransformFilter extends FilterBase {
    private ReturnCode targetRetCode;
    private boolean transformed = false;

    public TransformFilter(ReturnCode targetRetCode) {
      this.targetRetCode = targetRetCode;
    }

    @Override
    public ReturnCode filterCell(final Cell v) throws IOException {
      return targetRetCode;
    }

    @Override
    public Cell transformCell(Cell c) throws IOException {
      transformed = true;
      return super.transformCell(c);
    }

    public boolean getTransformed() {
      return this.transformed;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof TransformFilter)) {
        return false;
      }
      if (obj == this) {
        return true;
      }
      TransformFilter f = (TransformFilter) obj;
      return this.targetRetCode.equals(f.targetRetCode);
    }

    @Override
    public int hashCode() {
      return Objects.hash(this.targetRetCode);
    }
  }

  @Test
  public void testTransformCell() throws IOException {
    KeyValue kv = new KeyValue(Bytes.toBytes("row"), Bytes.toBytes("cf"), Bytes.toBytes("column1"),
      1, Bytes.toBytes("value"));

    // case MUST_PASS_ONE
    TransformFilter filter1 = new TransformFilter(ReturnCode.INCLUDE);
    TransformFilter filter2 = new TransformFilter(ReturnCode.NEXT_ROW);
    TransformFilter filter3 = new TransformFilter(ReturnCode.SEEK_NEXT_USING_HINT);
    FilterList filterList = new FilterList(Operator.MUST_PASS_ONE, filter1, filter2, filter3);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv));
    assertEquals(kv, filterList.transformCell(kv));
    assertEquals(true, filter1.getTransformed());
    assertEquals(false, filter2.getTransformed());
    assertEquals(false, filter3.getTransformed());

    // case MUST_PASS_ALL
    filter1 = new TransformFilter(ReturnCode.INCLUDE);
    filter2 = new TransformFilter(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW);
    filter3 = new TransformFilter(ReturnCode.INCLUDE_AND_NEXT_COL);
    filterList = new FilterList(Operator.MUST_PASS_ALL, filter1, filter2, filter3);

    assertEquals(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW, filterList.filterCell(kv));
    assertEquals(kv, filterList.transformCell(kv));
    assertEquals(true, filter1.getTransformed());
    assertEquals(true, filter2.getTransformed());
    assertEquals(true, filter3.getTransformed());
  }

  @Test
  public void testFilterListWithORWhenPassingCellMismatchPreviousRC() throws IOException {
    // Mainly test FilterListWithOR#calculateReturnCodeByPrevCellAndRC method with two sub-filters.
    KeyValue kv1 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fam"), Bytes.toBytes("a"),
      100, Bytes.toBytes("value"));
    KeyValue kv2 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fam"), Bytes.toBytes("a"), 99,
      Bytes.toBytes("value"));
    KeyValue kv3 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fam"), Bytes.toBytes("b"), 1,
      Bytes.toBytes("value"));
    KeyValue kv4 = new KeyValue(Bytes.toBytes("row1"), Bytes.toBytes("fan"), Bytes.toBytes("a"), 1,
      Bytes.toBytes("value"));
    Filter subFilter1 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter1.filterCell(kv1)).thenReturn(ReturnCode.INCLUDE_AND_NEXT_COL);
    Mockito.when(subFilter1.filterCell(kv2)).thenReturn(ReturnCode.NEXT_COL);
    Mockito.when(subFilter1.filterCell(kv3)).thenReturn(ReturnCode.INCLUDE_AND_NEXT_COL);
    Mockito.when(subFilter1.filterCell(kv4)).thenReturn(ReturnCode.INCLUDE_AND_NEXT_COL);

    Filter subFilter2 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter2.filterCell(kv1)).thenReturn(ReturnCode.SKIP);
    Mockito.when(subFilter2.filterCell(kv2)).thenReturn(ReturnCode.NEXT_ROW);
    Mockito.when(subFilter2.filterCell(kv3)).thenReturn(ReturnCode.NEXT_ROW);
    Mockito.when(subFilter2.filterCell(kv4)).thenReturn(ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW);

    Filter filterList = new FilterList(Operator.MUST_PASS_ONE, subFilter1, subFilter2);
    assertEquals(ReturnCode.INCLUDE, filterList.filterCell(kv1));
    assertEquals(ReturnCode.NEXT_COL, filterList.filterCell(kv2));
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv3));
    assertEquals(ReturnCode.INCLUDE_AND_NEXT_COL, filterList.filterCell(kv4));

    // One sub-filter will filterAllRemaining but other sub-filter will return SEEK_HINT
    subFilter1 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter1.filterAllRemaining()).thenReturn(true);
    Mockito.when(subFilter1.filterCell(kv1)).thenReturn(ReturnCode.NEXT_ROW);

    subFilter2 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter2.filterCell(kv1)).thenReturn(ReturnCode.SEEK_NEXT_USING_HINT);
    filterList = new FilterList(Operator.MUST_PASS_ONE, subFilter1, subFilter2);
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));

    // Two sub-filter returns SEEK_NEXT_USING_HINT, then we should return SEEK_NEXT_USING_HINT.
    subFilter1 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter1.filterCell(kv1)).thenReturn(ReturnCode.SEEK_NEXT_USING_HINT);

    subFilter2 = Mockito.mock(FilterBase.class);
    Mockito.when(subFilter2.filterCell(kv1)).thenReturn(ReturnCode.SEEK_NEXT_USING_HINT);
    filterList = new FilterList(Operator.MUST_PASS_ONE, subFilter1, subFilter2);
    assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(kv1));
  }
}
