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

import java.io.IOException;
import java.util.Arrays;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.KeyValueUtil;
import org.apache.hadoop.hbase.testclassification.FilterTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag(FilterTests.TAG)
@Tag(SmallTests.TAG)
public class TestFuzzyRowFilter {

  @Test
  public void testSatisfiesNoUnsafeForward() {

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, (byte) -128, 1, 0, 1 }, 0, 5,
        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, (byte) -128, 2, 0, 1 }, 0, 5,
        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfiesNoUnsafe(false,
      new byte[] { 1, 2, 1, 3, 3 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, 1, 1, 3, 0 }, // row to check
        0, 5, new byte[] { 1, 2, 0, 3 }, // fuzzy row
        new byte[] { 0, 0, 1, 0 })); // mask

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, 1, 1, 3, 0 }, 0, 5,
        new byte[] { 1, (byte) 245, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(false,
      new byte[] { 1, 2, 1, 0, 1 }, 0, 5, new byte[] { 0, 1, 2 }, new byte[] { 1, 0, 0 }));
  }

  @Test
  public void testSatisfiesForward() {

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(false,
      new byte[] { 1, (byte) -128, 1, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(false,
      new byte[] { 1, (byte) -128, 2, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(false,
      new byte[] { 1, 2, 1, 3, 3 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfies(false, new byte[] { 1, 1, 1, 3, 0 }, // row to check
        new byte[] { 1, 2, 0, 3 }, // fuzzy row
        new byte[] { -1, -1, 0, -1 })); // mask

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfies(false, new byte[] { 1, 1, 1, 3, 0 },
        new byte[] { 1, (byte) 245, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(false,
      new byte[] { 1, 2, 1, 0, 1 }, new byte[] { 0, 1, 2 }, new byte[] { 0, -1, -1 }));
  }

  @Test
  public void testSatisfiesReverse() {
    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(true,
      new byte[] { 1, (byte) -128, 1, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
      new byte[] { 1, (byte) -128, 2, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
      new byte[] { 2, 3, 1, 1, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(true,
      new byte[] { 1, 2, 1, 3, 3 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfies(true, new byte[] { 1, (byte) 245, 1, 3, 0 },
        new byte[] { 1, 1, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
      new byte[] { 1, 3, 1, 3, 0 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
      new byte[] { 2, 1, 1, 1, 0 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
      new byte[] { 1, 2, 1, 0, 1 }, new byte[] { 0, 1, 2 }, new byte[] { 0, -1, -1 }));
  }

  @Test
  public void testSatisfiesNoUnsafeReverse() {
    assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) -128, 1, 0, 1 }, 0, 5,
        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) -128, 2, 0, 1 }, 0, 5,
        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
      new byte[] { 2, 3, 1, 1, 1 }, 0, 5, new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfiesNoUnsafe(true,
      new byte[] { 1, 2, 1, 3, 3 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) 245, 1, 3, 0 }, 0, 5,
        new byte[] { 1, 1, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
      new byte[] { 1, 3, 1, 3, 0 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
      new byte[] { 2, 1, 1, 1, 0 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));

    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
      new byte[] { 1, 2, 1, 0, 1 }, 0, 5, new byte[] { 0, 1, 2 }, new byte[] { 1, 0, 0 }));
  }

  @Test
  public void testGetNextForFuzzyRuleForward() {
    assertNext(false, new byte[] { 0, 1, 2 }, // fuzzy row
      new byte[] { 0, -1, -1 }, // mask
      new byte[] { 1, 2, 1, 0, 1 }, // current
      new byte[] { 2, 1, 2 }); // expected next

    assertNext(false, new byte[] { 0, 1, 2 }, // fuzzy row
      new byte[] { 0, -1, -1 }, // mask
      new byte[] { 1, 1, 2, 0, 1 }, // current
      new byte[] { 1, 1, 2, 0, 2 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 2, 0 }, // fuzzy row
      new byte[] { 0, -1, 0, -1, 0 }, // mask
      new byte[] { 1, 0, 2, 0, 1 }, // current
      new byte[] { 1, 1, 0, 2 }); // expected next

    assertNext(false, new byte[] { 1, 0, 1 }, // fuzzy row
      new byte[] { -1, 0, -1 }, // mask
      new byte[] { 1, (byte) 128, 2, 0, 1 }, // current
      new byte[] { 1, (byte) 129, 1 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, 0, 1 }, // current
      new byte[] { 5, 1, 1, 1 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, 0, 1, 1 }, // current
      new byte[] { 5, 1, 0, 1, 2 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 0 }, // fuzzy row
      new byte[] { 0, -1, 0, 0 }, // mask
      new byte[] { 5, 1, (byte) 255, 1 }, // current
      new byte[] { 5, 1, (byte) 255, 2 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, (byte) 255, 1 }, // current
      new byte[] { 6, 1, 0, 1 }); // expected next

    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, (byte) 255, 0 }, // current
      new byte[] { 5, 1, (byte) 255, 1 }); // expected next

    assertNext(false, new byte[] { 5, 1, 1, 0 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 5, 1, (byte) 255, 1 }, // current
      new byte[] { 5, 1, (byte) 255, 2 }); // expected next

    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 1, 1, 2, 2 }, // current
      new byte[] { 1, 1, 2, 3 }); // expected next

    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 1, 1, 3, 2 }, // current
      new byte[] { 1, 1, 3, 3 }); // expected next

    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { 0, 0, 0, 0 }, // mask
      new byte[] { 1, 1, 2, 3 }, // current
      new byte[] { 1, 1, 2, 4 }); // expected next

    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { 0, 0, 0, 0 }, // mask
      new byte[] { 1, 1, 3, 2 }, // current
      new byte[] { 1, 1, 3, 3 }); // expected next

    assertNext(false, new byte[] { 1, 1, 0, 0 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 0, 1, 3, 2 }, // current
      new byte[] { 1, 1 }); // expected next

    // No next for this one
    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 2, 3, 1, 1, 1 }, // row to
                                                                                // check
      new byte[] { 1, 0, 1 }, // fuzzy row
      new byte[] { -1, 0, -1 })); // mask
    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 1, (byte) 245, 1, 3, 0 },
      new byte[] { 1, 1, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 1, 3, 1, 3, 0 },
      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 2, 1, 1, 1, 0 },
      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
  }

  @Test
  public void testGetNextForFuzzyRuleReverse() {
    // In these reverse cases for the next row key the last non-max byte should be increased
    // to make sure that the proper row is selected next by the scanner.
    // For example:
    // fuzzy row: 0,1,2
    // mask: 0,-1,-1
    // current: 1,2,1,0,1
    // next would be: 1,1,2
    // this has to be increased to 1,1,3 to make sure that the proper row is selected next.
    assertNext(true, new byte[] { 0, 1, 2 }, // fuzzy row
      new byte[] { 0, -1, -1 }, // mask
      new byte[] { 1, 2, 1, 0, 1 }, // current
      new byte[] { 1, 1, 3 }); // expected next

    // Preserve HBASE-28634 boundary hints. Lowering 1115 to 1114 would skip rows such as 111444.
    assertNext(true, Bytes.toBytes("111433"), // fuzzy row
      new byte[] { -1, -1, -1, -1, 0, 0 }, // mask
      Bytes.toBytes("1115"), // current
      Bytes.toBytes("1115")); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 2, 0 }, // fuzzy row
      new byte[] { 0, -1, 0, -1, 0 }, // mask
      new byte[] { 1, 2, 1, 3, 1 }, // current
      new byte[] { 1, 1, (byte) 255, 3 }); // expected next

    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
      new byte[] { -1, 0, -1 }, // mask
      new byte[] { 1, (byte) 128, 2, 0, 1 }, // current
      new byte[] { 1, (byte) 128, 2 }); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, 0, 2, 1 }, // current
      new byte[] { 5, 1, 0, 2 }); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, 0, 1 }, // current
      new byte[] { 4, 1, (byte) 255, 2 }); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, (byte) 255, 0 }, // current
      new byte[] { 5, 1, (byte) 254, 2 }); // expected next

    assertNext(true, new byte[] { 1, 1, 0, 0 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 2, 1, 3, 2 }, // current
      new byte[] { 1, 2 }); // expected next

    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
      new byte[] { -1, 0, -1 }, // mask
      new byte[] { 2, 3, 1, 1, 1 }, // row to check
      new byte[] { 1, (byte) 255, 2 }); // expected next

    assertNext(true, new byte[] { 1, 1, 0, 3 }, // fuzzy row
      new byte[] { -1, -1, 0, -1 }, // mask
      new byte[] { 1, (byte) 245, 1, 3, 0 }, // row to check
      new byte[] { 1, 1, (byte) 255, 4 }); // expected next

    assertNext(true, new byte[] { 1, 2, 0, 3 }, // fuzzy row
      new byte[] { -1, -1, 0, -1 }, // mask
      new byte[] { 1, 3, 1, 3, 0 }, // row to check
      new byte[] { 1, 2, (byte) 255, 4 }); // expected next

    assertNext(true, new byte[] { 1, 2, 0, 3 }, // fuzzy row
      new byte[] { -1, -1, 0, -1 }, // mask
      new byte[] { 2, 1, 1, 1, 0 }, // row to check
      new byte[] { 1, 2, (byte) 255, 4 }); // expected next

    // No cell before the current one satisfies the fuzzy row -> null.
    assertNull(FuzzyRowFilter.getNextForFuzzyRule(true, new byte[] { 1, 1, 1, 3, 0 },
      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
  }

  @Test
  public void testGetNextForFuzzyRuleReverseCanReturnCurrentRow() {
    // HBASE-28634 reverse adjustment can intentionally return the current row as a boundary hint.
    assertNext(true, new byte[] { 'a', 'a', 'a' }, // fuzzy row
      new byte[] { -1, 0, -1 }, // mask
      new byte[] { 'a', 'b', 'b' }, // current
      new byte[] { 'a', 'b', 'b' }); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 0 }, // fuzzy row
      new byte[] { 0, -1, 0, 0 }, // mask
      new byte[] { 5, 1, (byte) 255, 1 }, // current
      new byte[] { 5, 1, (byte) 255, 1 }); // expected next

    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
      new byte[] { -1, 0, -1 }, // mask
      new byte[] { 1, (byte) 128, 2 }, // row to check
      new byte[] { 1, (byte) 128, 2 }); // expected next

    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
      new byte[] { 0, -1, 0, -1 }, // mask
      new byte[] { 5, 1, 0, 2 }, // row to check
      new byte[] { 5, 1, 0, 2 }); // expected next

    assertNext(true, new byte[] { 5, 1, 1, 0 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 5, 1, (byte) 0xFF, 1 }, // row to check
      new byte[] { 5, 1, (byte) 0xFF, 1 }); // expected next

    assertNext(true, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { -1, -1, 0, 0 }, // mask
      new byte[] { 1, 1, 2, 2 }, // row to check
      new byte[] { 1, 1, 2, 2 }); // expected next

    assertNext(true, new byte[] { 1, 1, 1, 1 }, // fuzzy row
      new byte[] { 0, 0, 0, 0 }, // mask
      new byte[] { 1, 1, 2, 3 }, // row to check
      new byte[] { 1, 1, 2, 3 }); // expected next
  }

  private static void assertNext(boolean reverse, byte[] fuzzyRow, byte[] mask, byte[] current,
    byte[] expected) {
    KeyValue kv = KeyValueUtil.createFirstOnRow(current);
    byte[] nextForFuzzyRule = FuzzyRowFilter.getNextForFuzzyRule(reverse, kv.getRowArray(),
      kv.getRowOffset(), kv.getRowLength(), fuzzyRow, mask);
    assertEquals(Bytes.toStringBinary(expected), Bytes.toStringBinary(nextForFuzzyRule));
  }

  @Test
  public void testReverseFilterCellSkipsSameRowHint() {
    // The first non-matching row can seek to abb, but abb would recreate abb as its reverse hint.
    // The scanner should skip that non-matching row instead of seeking to the same row again.
    FuzzyRowFilter filter = newReverseFuzzyRowFilter();

    KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc"));
    assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filter.filterCell(abc));
    Cell hint = filter.getNextCellHint(abc);
    assertRow("abb", hint);

    KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb"));
    // filterCell handles the same-row hint by skipping the current non-matching row.
    assertEquals(Filter.ReturnCode.NEXT_ROW, filter.filterCell(abb));
  }

  @Test
  public void testReverseFilterListSkipsSameRowFuzzyHint() throws IOException {
    for (FilterList.Operator operator : Arrays.asList(FilterList.Operator.MUST_PASS_ALL,
      FilterList.Operator.MUST_PASS_ONE)) {
      FilterList filterList =
        new FilterList(operator, newReverseFuzzyRowFilter(), newReverseFuzzyRowFilter());

      KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc"));
      assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(abc));
      assertRow("abb", filterList.getNextCellHint(abc));

      KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb"));
      assertEquals(Filter.ReturnCode.NEXT_ROW, filterList.filterCell(abb));
    }
  }

  /**
   * Serializing a filter must not leak the internal mask encoding. On the unsafe path the stored
   * mask is {-1 (fixed), 0 (non-fixed)}; {@code toByteArray} must emit the public constructor {0,
   * 1} form so the round-tripped filter behaves identically (otherwise the leaked {-1, 0} is
   * reparsed as an all-fixed mask). Processing a cell first must not change this.
   */
  @Test
  public void testSerializationAfterFilterCellPreservesBehavior() throws Exception {
    FuzzyRowFilter original =
      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
    // Process a cell first to prove serialization is unaffected by scanning.
    original.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3 }));

    FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(original.toByteArray());
    // A row matching only via the wildcard position must still be INCLUDED after the round-trip.
    assertEquals(Filter.ReturnCode.INCLUDE,
      parsed.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
  }

  /**
   * Two filters built from the same rule (distinct array instances) must be equal and hash equally,
   * i.e. {@code equals}/{@code hashCode} must be content-based, not identity-based, and consistent
   * with each other and with the serialized ({0, 1}) form. This must also hold after one of them
   * has processed a cell.
   */
  @Test
  public void testEqualsConsistentAfterFilterCell() {
    FuzzyRowFilter fresh =
      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
    FuzzyRowFilter scanned =
      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
    scanned.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3 }));

    assertEquals(fresh, scanned);
    assertEquals(fresh.hashCode(), scanned.hashCode());
  }

  private static FuzzyRowFilter newReverseFuzzyRowFilter() {
    FuzzyRowFilter filter =
      new FuzzyRowFilter(Arrays.asList(new Pair<>(Bytes.toBytes("aaa"), new byte[] { 0, 1, 0 })));
    filter.setReversed(true);
    return filter;
  }

  private static void assertRow(String expected, Cell cell) {
    assertEquals(expected,
      Bytes.toString(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()));
  }
}
