package com.squareup.burst;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestClass;

import static com.squareup.burst.Util.checkNotNull;
import static java.util.Collections.unmodifiableList;

/**
 * A suite associated with a particular test class. Its children are {@link BurstRunner}s, each
 * representing a particular variation of that class.
 */
public final class BurstJUnit4 extends Suite {
  public BurstJUnit4(Class<?> cls) throws InitializationError {
    super(cls, explode(cls));
  }

  /*
   * ParentRunner's default filter implementation generates a hierarchy of test descriptions,
   * applies the filter to those descriptions, and removes any test nodes whose descriptions were
   * all filtered out.
   * <p>
   * This would be problematic for us since we generate non-standard test descriptions which include
   * parameter information. This implementation lets each {@link BurstRunner} child filter itself
   * via {@link BurstRunner#filter(Filter)}.
   */
  @Override public void filter(Filter filter) throws NoTestsRemainException {
    List<Runner> filteredChildren = ParentRunnerSpy.getFilteredChildren(this);
    // Iterate over a clone so that we can safely mutate the original.
    for (Runner child : new ArrayList<>(filteredChildren)) {
      try {
        filter.apply(child);
      } catch (NoTestsRemainException e) {
        filteredChildren.remove(child);
      }
    }
    if (filteredChildren.isEmpty()) {
      throw new NoTestsRemainException();
    }
  }

  static List<Runner> explode(Class<?> cls) throws InitializationError {
    checkNotNull(cls, "cls");

    TestClass testClass = new TestClass(cls);
    List<FrameworkMethod> testMethods = testClass.getAnnotatedMethods(Test.class);

    List<FrameworkMethod> burstMethods = new ArrayList<>(testMethods.size());
    for (FrameworkMethod testMethod : testMethods) {
      Method method = testMethod.getMethod();
      for (Enum<?>[] methodArgs : Burst.explodeArguments(method)) {
        burstMethods.add(new BurstMethod(method, methodArgs));
      }
    }

    TestConstructor constructor = BurstableConstructor.findSingle(cls);
    Enum<?>[][] constructorArgsList = Burst.explodeArguments(constructor);
    List<Runner> burstRunners = new ArrayList<>(constructorArgsList.length);
    for (Enum<?>[] constructorArgs : constructorArgsList) {
      burstRunners.add(new BurstRunner(cls, constructor, constructorArgs, burstMethods));
    }

    return unmodifiableList(burstRunners);
  }

  static String nameWithArguments(String name, Enum<?>[] arguments,
                                  Annotation[][] argumentAnnotations) {
    if (arguments.length == 0) {
      return name;
    }
    return name + '[' + Burst.friendlyName(arguments, argumentAnnotations) + ']';
  }
}