// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed 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 com.google.devtools.build.lib.rules.apple;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Tests that a given comparator (or the implementation of {@link Comparable}) is correct. To use,
 * repeatedly call {@link #addEqualityGroup(Object...)} with sets of objects that should be equal.
 * The calls to {@link #addEqualityGroup(Object...)} must be made in sorted order. Then call {@link
 * #testCompare()} to test the comparison. For example:
 *
 * <pre>{@code
 * new ComparatorTester()
 *     .addEqualityGroup(1)
 *     .addEqualityGroup(2)
 *     .addEqualityGroup(3)
 *     .testCompare();
 * }</pre>
 */
public class ComparatorTester {
  @SuppressWarnings({"unchecked", "rawtypes"})
  private final @Nullable Comparator comparator;

  /** The items that we are checking, stored as a sorted set of equivalence classes. */
  private final List<List<Object>> equalityGroups;

  /**
   * Creates a new instance that tests the order of objects using the natural order (as defined by
   * {@link Comparable}).
   */
  public ComparatorTester() {
    this(null);
  }

  /**
   * Creates a new instance that tests the order of objects using the given comparator. Or, if the
   * comparator is {@code null}, the natural ordering (as defined by {@link Comparable})
   */
  public ComparatorTester(@Nullable Comparator<?> comparator) {
    this.equalityGroups = new ArrayList<>();
    this.comparator = comparator;
  }

  /**
   * Adds a set of objects to the test which should all compare as equal. All of the elements in
   * {@code objects} must be greater than any element of {@code objects} in a previous call to
   * {@link #addEqualityGroup(Object...)}.
   *
   * @return {@code this} (to allow chaining of calls)
   */
  @CanIgnoreReturnValue
  public ComparatorTester addEqualityGroup(Object... objects) {
    Preconditions.checkNotNull(objects);
    Preconditions.checkArgument(objects.length > 0, "Array must not be empty");
    equalityGroups.add(ImmutableList.copyOf(objects));
    return this;
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private int compare(Object a, Object b) {
    int compareValue;
    if (comparator == null) {
      compareValue = ((Comparable) a).compareTo(b);
    } else {
      compareValue = comparator.compare(a, b);
    }
    return compareValue;
  }

  public final void testCompare() {
    for (int referenceIndex = 0; referenceIndex < equalityGroups.size(); referenceIndex++) {
      for (Object reference : equalityGroups.get(referenceIndex)) {
        testNullCompare(reference);
        testClassCast(reference);
        for (int otherIndex = 0; otherIndex < equalityGroups.size(); otherIndex++) {
          for (Object other : equalityGroups.get(otherIndex)) {
            assertThat(compare(reference, other))
                .isEqualTo(Integer.compare(referenceIndex, otherIndex));
          }
        }
      }
    }
  }

  private void testNullCompare(Object obj) {
    // Comparator does not require any specific behavior for null.
    if (comparator == null) {
      assertThrows(
          "Expected NullPointerException in " + obj + ".compare(null)",
          NullPointerException.class,
          () -> compare(obj, null));
    }
  }

  @SuppressWarnings("unchecked")
  private void testClassCast(Object obj) {
    if (comparator == null) {
      assertThrows(
          "Expected ClassCastException in " + obj + ".compareTo(otherObject)",
          ClassCastException.class,
          () -> compare(obj, ICanNotBeCompared.INSTANCE));
    }
  }

  private static final class ICanNotBeCompared {
    static final ComparatorTester.ICanNotBeCompared INSTANCE = new ICanNotBeCompared();
  }
}
