/*
 * 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.beam.sdk.io.range;

import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests of {@link ByteKey}. */
@RunWith(JUnit4.class)
public class ByteKeyTest {
  /* A big list of byte[] keys, in ascending sorted order. */
  static final ByteKey[] TEST_KEYS =
      new ByteKey[] {
        ByteKey.EMPTY,
        ByteKey.of(0),
        ByteKey.of(0, 1),
        ByteKey.of(0, 1, 1),
        ByteKey.of(0, 1, 2),
        ByteKey.of(0, 1, 2, 0xfe),
        ByteKey.of(0, 1, 3, 0xfe),
        ByteKey.of(0, 0xfe, 0xfe, 0xfe),
        ByteKey.of(0, 0xfe, 0xfe, 0xff),
        ByteKey.of(0, 0xfe, 0xff, 0),
        ByteKey.of(0, 0xff, 0xff, 0),
        ByteKey.of(0, 0xff, 0xff, 1),
        ByteKey.of(0, 0xff, 0xff, 0xfe),
        ByteKey.of(0, 0xff, 0xff, 0xff),
        ByteKey.of(1),
        ByteKey.of(1, 2),
        ByteKey.of(1, 2, 3),
        ByteKey.of(3),
        ByteKey.of(0xdd),
        ByteKey.of(0xfe),
        ByteKey.of(0xfe, 0xfe),
        ByteKey.of(0xfe, 0xff),
        ByteKey.of(0xff),
        ByteKey.of(0xff, 0),
        ByteKey.of(0xff, 0xfe),
        ByteKey.of(0xff, 0xff),
        ByteKey.of(0xff, 0xff, 0xff),
        ByteKey.of(0xff, 0xff, 0xff, 0xff),
      };

  /**
   * Tests {@link ByteKey#compareTo(ByteKey)} using exhaustive testing within a large sorted list of
   * keys.
   */
  @Test
  public void testCompareToExhaustive() {
    // Verify that the comparison gives the correct result for all values in both directions.
    for (int i = 0; i < TEST_KEYS.length; ++i) {
      for (int j = 0; j < TEST_KEYS.length; ++j) {
        ByteKey left = TEST_KEYS[i];
        ByteKey right = TEST_KEYS[j];
        int cmp = left.compareTo(right);
        if (i < j && !(cmp < 0)) {
          fail(
              String.format(
                  "Expected that cmp(%s, %s) < 0, got %d [i=%d, j=%d]", left, right, cmp, i, j));
        } else if (i == j && !(cmp == 0)) {
          fail(
              String.format(
                  "Expected that cmp(%s, %s) == 0, got %d [i=%d, j=%d]", left, right, cmp, i, j));
        } else if (i > j && !(cmp > 0)) {
          fail(
              String.format(
                  "Expected that cmp(%s, %s) > 0, got %d [i=%d, j=%d]", left, right, cmp, i, j));
        }
      }
    }
  }

  /** Tests {@link ByteKey#equals}. */
  @Test
  public void testEquals() {
    // Verify that the comparison gives the correct result for all values in both directions.
    for (int i = 0; i < TEST_KEYS.length; ++i) {
      for (int j = 0; j < TEST_KEYS.length; ++j) {
        ByteKey left = TEST_KEYS[i];
        ByteKey right = TEST_KEYS[j];
        boolean eq = left.equals(right);
        if (i == j) {
          assertTrue(String.format("Expected that %s is equal to itself.", left), eq);
          assertTrue(
              String.format("Expected that %s is equal to a copy of itself.", left),
              left.equals(ByteKey.copyFrom(right.getValue())));
        } else {
          assertFalse(String.format("Expected that %s is not equal to %s", left, right), eq);
        }
      }
    }
  }

  /** Tests {@link ByteKey#hashCode}. */
  @Test
  public void testHashCode() {
    // Verify that the hashCode is equal when i==j, and usually not equal otherwise.
    int collisions = 0;
    for (int i = 0; i < TEST_KEYS.length; ++i) {
      int left = TEST_KEYS[i].hashCode();
      int leftClone = ByteKey.copyFrom(TEST_KEYS[i].getValue()).hashCode();
      assertEquals(
          String.format("Expected same hash code for %s and a copy of itself", TEST_KEYS[i]),
          left,
          leftClone);
      for (int j = i + 1; j < TEST_KEYS.length; ++j) {
        int right = TEST_KEYS[j].hashCode();
        if (left == right) {
          ++collisions;
        }
      }
    }
    int totalUnequalTests = TEST_KEYS.length * (TEST_KEYS.length - 1) / 2;
    assertThat("Too many hash collisions", collisions, lessThan(totalUnequalTests / 2));
  }

  /** Tests {@link ByteKey#toString}. */
  @Test
  public void testToString() {
    assertEquals("[]", ByteKey.EMPTY.toString());
    assertEquals("[00]", ByteKey.of(0).toString());
    assertEquals("[0000]", ByteKey.of(0x00, 0x00).toString());
    assertEquals(
        "[0123456789abcdef]",
        ByteKey.of(0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef).toString());
  }

  /** Tests {@link ByteKey#isEmpty}. */
  @Test
  public void testIsEmpty() {
    assertTrue("[] is empty", ByteKey.EMPTY.isEmpty());
    assertFalse("[00]", ByteKey.of(0).isEmpty());
  }

  /** Tests {@link ByteKey#getBytes}. */
  @Test
  public void testGetBytes() {
    assertTrue("[] equal after getBytes", Arrays.equals(new byte[] {}, ByteKey.EMPTY.getBytes()));
    assertTrue(
        "[00] equal after getBytes", Arrays.equals(new byte[] {0x00}, ByteKey.of(0x00).getBytes()));
  }
}
