/*
 * Copyright © 2009  Esko Luontola, www.orfjackal.net
 *
 * 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 net.orfjackal.nestedjunit;

import org.junit.Test;
import org.junit.runner.*;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.*;
import org.junit.runners.model.InitializationError;

import java.lang.reflect.Method;
import java.util.*;

/**
 * Allows organizing JUnit 4 tests into member classes. This makes it possible
 * to write tests in a more Behaviour-Driven Development (BDD) style:
 * <pre><code>
 * &#64;RunWith(NestedJUnit4.class)
 * public class WerewolfTest extends Assert {
 *     public class Given_the_moon_is_full {
 *         &#64;Before public void When_you_walk_in_the_woods() {
 *             ...
 *         }
 *         &#64;Test public void Then_you_can_hear_werewolves_howling() {
 *             ...
 *         }
 *         &#64;Test public void Then_you_wish_you_had_a_silver_bullet() {
 *             ...
 *         }
 *     }
 *     public class Given_the_moon_is_not_full {
 *         &#64;Before public void When_you_walk_in_the_woods() {
 *             ...
 *         }
 *         &#64;Test public void Then_you_do_not_hear_any_werewolves() {
 *             ...
 *         }
 *         &#64;Test public void Then_you_are_not_afraid() {
 *             ...
 *         }
 *     }
 * }
 * </code></pre>
 *
 * @author Esko Luontola
 */
public class NestedJUnit4 extends ParentRunner<Runner> {

    private final List<Runner> children = new ArrayList<Runner>();

    public NestedJUnit4(Class<?> testClass) throws InitializationError {
        super(testClass);
        addToChildrenAllNestedClassesWithTests(testClass);
    }

    private void addToChildrenAllNestedClassesWithTests(Class<?> testClass) throws InitializationError {
        for (Class<?> child : (Class<?>[]) testClass.getDeclaredClasses()) {
            if (containsTests(child)) {
                children.add(new NestedJUnit4ClassRunner(child));
            }
        }
    }

    private boolean containsTests(Class<?> clazz) {
        for (Method method : clazz.getMethods()) {
            if (method.getAnnotation(Test.class) != null) {
                return true;
            }
        }
        return false;
    }

    protected List<Runner> getChildren() {
        return children;
    }

    protected Description describeChild(Runner child) {
        return child.getDescription();
    }

    protected void runChild(Runner child, RunNotifier notifier) {
        child.run(notifier);
    }


    private static class NestedJUnit4ClassRunner extends BlockJUnit4ClassRunner {

        public NestedJUnit4ClassRunner(Class<?> childClass) throws InitializationError {
            super(childClass);
        }

        protected void validateConstructor(List<Throwable> errors) {
            validateOnlyOneConstructor(errors);
            validateNonStaticInnerClassWithDefaultConstructor(errors);
        }

        private void validateNonStaticInnerClassWithDefaultConstructor(List<Throwable> errors) {
            try {
                getTestClass().getJavaClass().getConstructor(getParentClass());
            } catch (NoSuchMethodException e) {
                String gripe = "Nested test classes should be non-static and have a public zero-argument constructor";
                errors.add(new Exception(gripe));
            }
        }

        protected Object createTest() throws Exception {
            Object parent = getParentClass().newInstance();
            return getTestClass().getOnlyConstructor().newInstance(parent);
        }

        private Class<?> getParentClass() {
            return getTestClass().getJavaClass().getEnclosingClass();
        }
    }
}

@@ -666,20 +666,65 @@
 g.junit.
-Test
+*;%0Aimport org.junit.internal.runners.statements.*
 ;%0Aimport
@@ -853,35 +853,17 @@
 s.model.
-InitializationError
+*
 ;%0A%0Aimpor
@@ -1988,24 +1988,69 @@
 %3CRunner%3E %7B%0A%0A
+    private final TestClass parentTestClass;%0A
     private 
@@ -2196,32 +2196,84 @@
 per(testClass);%0A
+        parentTestClass = new TestClass(testClass);%0A
         addToChi
@@ -3178,23 +3178,16 @@
 private 
-static 
 class Ne
@@ -3238,24 +3238,69 @@
 ssRunner %7B%0A%0A
+        private Object parentOfCurrentTest;%0A%0A
         publ
@@ -3793,25 +3793,39 @@
 tructor(
-getP
+p
 arent
+TestClass.getJava
 Class())
@@ -4141,33 +4141,53 @@
     
-Object parent = getParent
+parentOfCurrentTest = parentTestClass.getJava
 Clas
@@ -4278,16 +4278,29 @@
 e(parent
+OfCurrentTest
 );%0A     
@@ -4319,41 +4319,261 @@
   pr
-ivate Class%3C?%3E getParentClass() %7B
+otected Statement methodBlock(FrameworkMethod method) %7B%0A            Statement statement = super.methodBlock(method);%0A            statement = withParentBefores(statement);%0A            statement = withParentAfters(statement);%0A            return statement;
 %0A   
@@ -4581,67 +4581,553 @@
     
+%7D%0A%0A
     
-return getTestClass().getJava
+    private Statement withParentBefores(Statement statement) %7B%0A            List%3CFrameworkMethod%3E befores = parentTestClass.getAnnotatedMethods(Before.class);%0A            return befores.isEmpty() ? statement : new RunBefores(statement, befores, parentOfCurrentTest);%0A        %7D%0A%0A        private Statement withParentAfters(Statement statement) %7B%0A            List%3CFrameworkMethod%3E afters = parentTest
 Class
-()
 .get
-EnclosingClass(
+AnnotatedMethods(After.class);%0A            return afters.isEmpty() ? statement : new RunAfters(statement, afters, parentOfCurrentTest
 );%0A 

enable @Before and @After in the surrounding parent class