package org.junit.tests.experimental.rules;

import static org.junit.Assert.assertEquals;
import static org.junit.internal.runners.rules.RuleMemberValidator.CLASS_RULE_METHOD_VALIDATOR;
import static org.junit.internal.runners.rules.RuleMemberValidator.CLASS_RULE_VALIDATOR;
import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_METHOD_VALIDATOR;
import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_VALIDATOR;

import java.util.ArrayList;
import java.util.List;

import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatchman;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

public class RuleMemberValidatorTest {
    private final List<Throwable> errors = new ArrayList<Throwable>();

    @Test
    public void rejectProtectedClassRule() {
        TestClass target = new TestClass(TestWithProtectedClassRule.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'temporaryFolder' must be public.");
    }

    public static class TestWithProtectedClassRule {
        @ClassRule
        protected static TestRule temporaryFolder = new TemporaryFolder();
    }

    @Test
    public void rejectNonStaticClassRule() {
        TestClass target = new TestClass(TestWithNonStaticClassRule.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'temporaryFolder' must be static.");
    }

    public static class TestWithNonStaticClassRule {
        @ClassRule
        public TestRule temporaryFolder = new TemporaryFolder();
    }

    @Test
    public void acceptStaticTestRuleThatIsAlsoClassRule() {
        TestClass target = new TestClass(TestWithStaticClassAndTestRule.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class TestWithStaticClassAndTestRule {
        @ClassRule
        @Rule
        public static TestRule temporaryFolder = new TemporaryFolder();
    }

    @Test
    public void rejectClassRuleInNonPublicClass() {
        TestClass target = new TestClass(NonPublicTestWithClassRule.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'temporaryFolder' must be declared in a public class.");
    }

    static class NonPublicTestWithClassRule {
        @ClassRule
        public static TestRule temporaryFolder = new TemporaryFolder();
    }
    
    /**
     * If there is any property annotated with @ClassRule then it must implement
     * {@link TestRule}
     * 
     * <p>This case has been added with 
     * <a href="https://github.com/junit-team/junit/issues/1019">Issue #1019</a>
     */
    @Test
    public void rejectClassRuleThatIsImplemetationOfMethodRule() {
        TestClass target = new TestClass(TestWithClassRuleIsImplementationOfMethodRule.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'classRule' must implement TestRule.");
    }
    
    public static class TestWithClassRuleIsImplementationOfMethodRule {
        @ClassRule
        public static MethodRule classRule = new MethodRule() {
            
            public Statement apply(Statement base, FrameworkMethod method, Object target) {
                return base;
            }
        };
    }

    /**
     * If there is any method annotated with @ClassRule then it must return an 
     * implementation of {@link TestRule}
     * 
     * <p>This case has been added with 
     * <a href="https://github.com/junit-team/junit/issues/1019">Issue #1019</a>
     */
    @Test
    public void rejectClassRuleThatReturnsImplementationOfMethodRule() {
        TestClass target = new TestClass(TestWithClassRuleMethodThatReturnsMethodRule.class);
        CLASS_RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'methodRule' must return an implementation of TestRule.");
    }

    public static class TestWithClassRuleMethodThatReturnsMethodRule {
        @ClassRule
        public static MethodRule methodRule() {
            return new MethodRule() {
                
                public Statement apply(Statement base, FrameworkMethod method, Object target) {
                    return base;
                }
            };
        }
    }
    
    /**
     * If there is any property annotated with @ClassRule then it must implement
     * {@link TestRule}
     * 
     * <p>This case has been added with 
     * <a href="https://github.com/junit-team/junit/issues/1019">Issue #1019</a>
     */
    @Test
    public void rejectClassRuleIsAnArbitraryObject() throws Exception {
        TestClass target = new TestClass(TestWithClassRuleIsAnArbitraryObject.class);
        CLASS_RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'arbitraryObject' must implement TestRule.");
    }

    public static class TestWithClassRuleIsAnArbitraryObject {
        @ClassRule
        public static Object arbitraryObject = 1;
    }
    
    /**
     * If there is any method annotated with @ClassRule then it must return an 
     * implementation of {@link TestRule}
     * 
     * <p>This case has been added with 
     * <a href="https://github.com/junit-team/junit/issues/1019">Issue #1019</a> 
     */
    @Test
    public void rejectClassRuleMethodReturnsAnArbitraryObject() throws Exception {
        TestClass target = new TestClass(TestWithClassRuleMethodReturnsAnArbitraryObject.class);
        CLASS_RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'arbitraryObject' must return an implementation of TestRule.");
    }

    public static class TestWithClassRuleMethodReturnsAnArbitraryObject {
        @ClassRule
        public static Object arbitraryObject() {
            return 1;
        }
    }
    
    @Test
    public void acceptNonStaticTestRule() {
        TestClass target = new TestClass(TestWithNonStaticTestRule.class);
        RULE_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class TestWithNonStaticTestRule {
        @Rule
        public TestRule temporaryFolder = new TemporaryFolder();
    }

    @Test
    public void rejectStaticTestRule() {
        TestClass target = new TestClass(TestWithStaticTestRule.class);
        RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'temporaryFolder' must not be static or it must be annotated with @ClassRule.");
    }

    public static class TestWithStaticTestRule {
        @Rule
        public static TestRule temporaryFolder = new TemporaryFolder();
    }

    @Test
    public void rejectStaticMethodRule() {
        TestClass target = new TestClass(TestWithStaticMethodRule.class);
        RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'testWatchman' must not be static.");
    }

    public static class TestWithStaticMethodRule {
        @SuppressWarnings("deprecation")
        @Rule
        public static MethodRule testWatchman = new TestWatchman();
    }
    
    @Test
    public void acceptMethodRule() throws Exception {
        TestClass target = new TestClass(TestWithMethodRule.class);
        RULE_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class TestWithMethodRule {
        @Rule
        public MethodRule temporaryFolder = new MethodRule() {
            public Statement apply(Statement base, FrameworkMethod method,
                    Object target) {
                return null;
            }
        };
    }

    @Test
    public void rejectArbitraryObjectWithRuleAnnotation() throws Exception {
        TestClass target = new TestClass(TestWithArbitraryObjectWithRuleAnnotation.class);
        RULE_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'arbitraryObject' must implement MethodRule or TestRule.");
    }

    public static class TestWithArbitraryObjectWithRuleAnnotation {
        @Rule
        public Object arbitraryObject = 1;
    }

    @Test
    public void methodRejectProtectedClassRule() {
        TestClass target = new TestClass(MethodTestWithProtectedClassRule.class);
        CLASS_RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'getTemporaryFolder' must be public.");
    }

    public static class MethodTestWithProtectedClassRule {
        @ClassRule
        protected static TestRule getTemporaryFolder() {
            return new TemporaryFolder();
        }
    }

    @Test
    public void methodRejectNonStaticClassRule() {
        TestClass target = new TestClass(MethodTestWithNonStaticClassRule.class);
        CLASS_RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @ClassRule 'getTemporaryFolder' must be static.");
    }

    public static class MethodTestWithNonStaticClassRule {
        @ClassRule
        public TestRule getTemporaryFolder() {
            return new TemporaryFolder();
        }
    }

    @Test
    public void acceptMethodStaticTestRuleThatIsAlsoClassRule() {
        TestClass target = new TestClass(MethodTestWithStaticClassAndTestRule.class);
        CLASS_RULE_METHOD_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class MethodTestWithStaticClassAndTestRule {
        @ClassRule
        @Rule
        public static TestRule getTemporaryFolder() {
            return new TemporaryFolder();
        }
    }

    @Test
    public void acceptMethodNonStaticTestRule() {
        TestClass target = new TestClass(TestMethodWithNonStaticTestRule.class);
        RULE_METHOD_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class TestMethodWithNonStaticTestRule {
        @Rule
        public TestRule getTemporaryFolder() {
            return new TemporaryFolder();
        }
    }

    @Test
    public void rejectMethodStaticTestRule() {
        TestClass target = new TestClass(TestMethodWithStaticTestRule.class);
        RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'getTemporaryFolder' must not be static or it must be annotated with @ClassRule.");
    }

    public static class TestMethodWithStaticTestRule {
        @Rule
        public static TestRule getTemporaryFolder() {
            return new TemporaryFolder();
        }
    }

    @Test
    public void rejectMethodStaticMethodRule() {
        TestClass target = new TestClass(TestMethodWithStaticMethodRule.class);
        RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'getTestWatchman' must not be static.");
    }

    public static class TestMethodWithStaticMethodRule {
        @SuppressWarnings("deprecation")
        @Rule
        public static MethodRule getTestWatchman() { return new TestWatchman(); }
    }

    @Test
    public void methodAcceptMethodRuleMethod() throws Exception {
        TestClass target = new TestClass(MethodTestWithMethodRule.class);
        RULE_METHOD_VALIDATOR.validate(target, errors);
        assertNumberOfErrors(0);
    }

    public static class MethodTestWithMethodRule {
        @Rule
        public MethodRule getTemporaryFolder() {
            return new MethodRule() {
                public Statement apply(Statement base, FrameworkMethod method,
                        Object target) {
                    return null;
                }
            };
        }
    }

    @Test
    public void methodRejectArbitraryObjectWithRuleAnnotation() throws Exception {
        TestClass target = new TestClass(MethodTestWithArbitraryObjectWithRuleAnnotation.class);
        RULE_METHOD_VALIDATOR.validate(target, errors);
        assertOneErrorWithMessage("The @Rule 'getArbitraryObject' must return an implementation of MethodRule or TestRule.");
    }

    public static class MethodTestWithArbitraryObjectWithRuleAnnotation {
        @Rule
        public Object getArbitraryObject() {
            return 1;
        }
    }

    private void assertOneErrorWithMessage(String message) {
        assertNumberOfErrors(1);
        assertEquals("Wrong error message:", message, errors.get(0).getMessage());
    }

    private void assertNumberOfErrors(int numberOfErrors) {
        assertEquals("Wrong number of errors:", numberOfErrors, errors.size());
    }
}
