/**
 * Copyright 2010 Wealthfront Inc. 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.kaching.platform.testing;

import static java.io.File.separator;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import junit.framework.JUnit4TestAdapter;
import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.junit.Ignore;
import org.junit.runner.RunWith;

/**
 * A {@link TestSuite} builder.
 */
public class TestSuiteBuilder {

  private final TestSuite testSuite = new TestSuite();
  private final File root;

  private TestSuiteBuilder(File root) {
    this.root = root;
  }
  
  /**
   * Builds and returns a test suite by traversing the root directory
   * recursively and finding all the tests.
   */
  public static TestSuite buildFromRoot(File root) {
    final String testCaseBuildingSuite = builderCaller();
    return buildFromRoot(root, new TestFilter() {
      public boolean shouldIncludeTest(Class<?> test) {
        return !test.getName().equals(testCaseBuildingSuite);
      }
    });
  }
  
  /**
   * Builds and returns a test suite by traversing the root directory
   * recursively and finding all the tests matching the {@code filter}.
   */
  public static TestSuite buildFromRoot(File root, TestFilter filter) {
    TestSuiteBuilder builder = new TestSuiteBuilder(root);
    builder.addTestsInDirectory(root, filter);
    return builder.testSuite;
  }

  static String builderCaller() {
    String builderClassName = TestSuiteBuilder.class.getName();
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    boolean found = false;
    for (int i = 0; i < stackTrace.length; i++) {
      String className = stackTrace[i].getClassName();
      found = found || className.equals(builderClassName);
      if (found && !className.equals(builderClassName)) {
        return className;
      }
    }
    throw new IllegalStateException("unreachable code");
  }

  private void addTestsInDirectory(File directory, TestFilter filter) {
    for (File file : directory.listFiles()) {
      if (file.isDirectory()) {
        addTestsInDirectory(file, filter);
      } else if (file.getName().endsWith(".java")) {
        Class<?> clazz = forName(file);
        if (filter.shouldIncludeTest(clazz)) {
          if (TestCase.class.isAssignableFrom(clazz) && clazz.getAnnotation(Ignore.class) == null) {
            testSuite.addTestSuite(castToTestCase(clazz));
          } else if (hasTests(clazz) && filter.shouldIncludeTest(clazz)) {
            JUnit4TestAdapter test = new JUnit4TestAdapter(clazz);
            testSuite.addTest(test);
          }
        }
      }
    }
  }

  private boolean hasTests(Class<?> clazz) {
    for (Method method : clazz.getDeclaredMethods()) {
      if (method.getAnnotation(org.junit.Test.class) != null) {
        return true;
      }
    }
    for (Annotation a : clazz.getAnnotations()) {
      if (a.annotationType().equals(RunWith.class)) {
        return true;
      }
    }
    return false;
  }

  @SuppressWarnings("unchecked")
  private static Class<? extends TestCase> castToTestCase(Class<?> clazz) {
    return (Class<? extends TestCase>) clazz;
  }

  private Class<?> forName(File file) {
    String name = file.toString();
    name = name.replace(root.toString() + separator, "");
    name = name.replace(".java", "");
    name = name.replace(separator, ".");
    try {
      return Class.forName(name);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

}
