// Copyright 2010 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.testing.junit.runner.sharding.testing;

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

import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.manipulation.Filter;

/**
 * Common base class for all sharding filter tests.
 */
public abstract class ShardingFilterTestCase extends TestCase {
  static final List<Description> TEST_DESCRIPTIONS = createGenericTestCaseDescriptions(6);

  /**
   * Returns a filter of the subclass type using the given descriptions,
   * shard index, and total number of shards.
   */
  protected abstract ShardingFilterFactory createShardingFilterFactory();

  public final void testShardingIsCompleteAndPartitioned_oneShard() {
    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 1), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsStable_oneShard() {
    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 1), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsCompleteAndPartitioned_moreTestsThanShards() {
    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 5), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsStable_moreTestsThanShards() {
    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 5), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsCompleteAndPartitioned_sameNumberOfTestsAndShards() {
    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 6), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsStable_sameNumberOfTestsAndShards() {
    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 6), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsCompleteAndPartitioned_moreShardsThanTests() {
    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 7), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsStable_moreShardsThanTests() {
    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 7), TEST_DESCRIPTIONS);
  }

  public final void testShardingIsCompleteAndPartitioned_duplicateDescriptions() {
    List<Description> descriptions = new ArrayList<>();
    descriptions.addAll(createGenericTestCaseDescriptions(6));
    descriptions.addAll(createGenericTestCaseDescriptions(6));
    assertShardingIsCompleteAndPartitioned(createFilters(descriptions, 7), descriptions);
  }

  public final void testShardingIsStable_duplicateDescriptions() {
    List<Description> descriptions = new ArrayList<>();
    descriptions.addAll(createGenericTestCaseDescriptions(6));
    descriptions.addAll(createGenericTestCaseDescriptions(6));
    assertShardingIsStable(createFilters(descriptions, 7), descriptions);
  }

  public final void testShouldRunTestSuite() {
    Description testSuiteDescription = createTestSuiteDescription();
    Filter filter = createShardingFilterFactory().createFilter(TEST_DESCRIPTIONS, 0, 1);
    assertThat(filter.shouldRun(testSuiteDescription)).isTrue();
  }

  /**
   * Creates a list of generic test case descriptions.
   *
   * @param numDescriptions the number of generic test descriptions to add to the list.
   */
  public static List<Description> createGenericTestCaseDescriptions(int numDescriptions) {
    List<Description> descriptions = new ArrayList<>();
    for (int i = 0; i < numDescriptions; i++) {
      descriptions.add(Description.createTestDescription(Test.class, "test" + i));
    }
    return descriptions;
  }

  protected static final List<Filter> createFilters(List<Description> descriptions, int numShards,
      ShardingFilterFactory factory) {
    List<Filter> filters = new ArrayList<>();
    for (int shardIndex = 0; shardIndex < numShards; shardIndex++) {
      filters.add(factory.createFilter(descriptions, shardIndex, numShards));
    }
    return filters;
  }

  protected final List<Filter> createFilters(List<Description> descriptions, int numShards) {
    return createFilters(descriptions, numShards, createShardingFilterFactory());
  }

  protected static void assertThrowsExceptionForUnknownDescription(Filter filter) {
    assertThrows(
        IllegalArgumentException.class,
        () -> filter.shouldRun(Description.createTestDescription(Object.class, "unknown")));
  }

  /**
   * Simulates test sharding with the given filters and test descriptions.
   *
   * @param filters a list of filters, one per test shard
   * @param descriptions a list of test descriptions
   * @return a mapping from each filter to the descriptions of the tests that would be run
   *   by the shard associated with that filter.
   */
  protected static Map<Filter, List<Description>> simulateTestRun(List<Filter> filters,
      List<Description> descriptions) {
    Map<Filter, List<Description>> descriptionsRun = new HashMap<>();
    for (Filter filter : filters) {
      for (Description description : descriptions) {
        if (filter.shouldRun(description)) {
          addDescriptionForFilterToMap(descriptionsRun, filter, description);
        }
      }
    }
    return descriptionsRun;
  }

  /**
   * Simulates test sharding with the given filters and test descriptions, for a
   * set of test descriptions that is in a different order in every test shard.
   *
   * @param filters a list of filters, one per test shard
   * @param descriptions a list of test descriptions
   * @return a mapping from each filter to the descriptions of the tests that would be run
   *   by the shard associated with that filter.
   */
  protected static Map<Filter, List<Description>> simulateSelfRandomizingTestRun(
      List<Filter> filters, List<Description> descriptions) {
    if (descriptions.isEmpty()) {
      return new HashMap<>();
    }
    Deque<Description> mutatingDescriptions = new LinkedList<>(descriptions);
    Map<Filter, List<Description>> descriptionsRun = new HashMap<>();

    for (Filter filter : filters) {
      // rotate the queue so that each filter gets the descriptions in a different order
      mutatingDescriptions.addLast(mutatingDescriptions.pollFirst());
      for (Description description : descriptions) {
        if (filter.shouldRun(description)) {
          addDescriptionForFilterToMap(descriptionsRun, filter, description);
        }
      }
    }
    return descriptionsRun;
  }

  /**
   * Creates a test suite description (a Description that returns true
   * when {@link org.junit.runner.Description#isSuite()} is called.)
   */
  protected static Description createTestSuiteDescription() {
    Description testSuiteDescription = Description.createSuiteDescription("testSuite");
    testSuiteDescription.addChild(Description.createSuiteDescription("testCase"));
    return testSuiteDescription;
  }

  /**
   * Tests that the sharding is complete (each test is run at least once) and
   * partitioned (each test is run at most once) -- in other words, that
   * each test is run exactly once.  This is a requirement of all test
   * sharding functions.
   */
  protected static void assertShardingIsCompleteAndPartitioned(List<Filter> filters,
      List<Description> descriptions) {
    Map<Filter, List<Description>> run = simulateTestRun(filters, descriptions);
    assertThatCollectionContainsExactlyElementsInList(getAllValuesInMap(run), descriptions);

    run = simulateSelfRandomizingTestRun(filters, descriptions);
    assertThatCollectionContainsExactlyElementsInList(getAllValuesInMap(run), descriptions);
  }
  /**
   * Tests that sharding is stable for the given filters, regardless of the
   * ordering of the descriptions.  This is useful for verifying that sharding
   * works with self-randomizing test suites, and a requirement of all test
   * sharding functions.
   */
  protected static void assertShardingIsStable(
      List<Filter> filters, List<Description> descriptions) {
    Map<Filter, List<Description>> run1 = simulateTestRun(filters, descriptions);
    Map<Filter, List<Description>> run2 = simulateTestRun(filters, descriptions);
    assertThat(run2).isEqualTo(run1);

    Map<Filter, List<Description>> randomizedRun1 =
        simulateSelfRandomizingTestRun(filters, descriptions);
    Map<Filter, List<Description>> randomizedRun2 =
        simulateSelfRandomizingTestRun(filters, descriptions);
    assertThat(randomizedRun2).isEqualTo(randomizedRun1);
  }

  private static void addDescriptionForFilterToMap(
      Map<Filter, List<Description>> descriptionsRun, Filter filter, Description description) {
    List<Description> descriptions = descriptionsRun.get(filter);
    if (descriptions == null) {
      descriptions = new ArrayList<>();
      descriptionsRun.put(filter, descriptions);
    }
    descriptions.add(description);
  }

  private static Collection<Description> getAllValuesInMap(Map<Filter, List<Description>> map) {
    Collection<Description> allDescriptions = new ArrayList<>();
    for (List<Description> descriptions : map.values()) {
      allDescriptions.addAll(descriptions);
    }
    return allDescriptions;
  }

  /**
   * Returns whether the Collection and the List contain exactly the same elements with the same
   * frequency, ignoring the ordering.
   */
  private static void assertThatCollectionContainsExactlyElementsInList(
      Collection<Description> actual, List<Description> expectedDescriptions) {
    String basicAssertionMessage = "Elements of collection " + actual + " are not the same as the "
        + "elements of expected list " + expectedDescriptions + ". ";
    if (actual.size() != expectedDescriptions.size()) {
      throw new AssertionError(basicAssertionMessage + "The number of elements is different.");
    }

    List<Description> actualDescriptions = new ArrayList<Description>(actual);
    // Keeps track of already reviewed descriptions, so they won't be checked again when next
    // encountered.
    // Note: this algorithm has O(n^2) time complexity and will be slow for large inputs.
    Set<Description> reviewedDescriptions = new HashSet<>();
    for (int i = 0; i < actual.size(); i++) {
      Description currDescription = actualDescriptions.get(i);
      // If already reviewed, skip.
      if (reviewedDescriptions.contains(currDescription)) {
        continue;
      }
      int actualFreq = 0;
      int expectedFreq = 0;
      // Count the frequency of the current description in both lists.
      for (int j = 0; j < actual.size(); j++) {
        if (currDescription.equals(actualDescriptions.get(j))) {
          actualFreq++;
        }
        if (currDescription.equals(expectedDescriptions.get(j))) {
          expectedFreq++;
        }
      }
      if (actualFreq < expectedFreq) {
        throw new AssertionError(basicAssertionMessage + "There are " + (expectedFreq - actualFreq)
            + " missing occurrences of " + currDescription + ".");
      } else if (actualFreq > expectedFreq) {
        throw new AssertionError(basicAssertionMessage + "There are " + (actualFreq - expectedFreq)
            + " unexpected occurrences of " + currDescription + ".");
      }
      reviewedDescriptions.add(currDescription);
    }
  }
}
