package org.junit.tests.experimental.rules;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.experimental.results.PrintableResult.testResult;
import static org.junit.experimental.results.ResultMatchers.hasSingleFailureContaining;
import static org.junit.experimental.results.ResultMatchers.isSuccessful;

import java.util.LinkedList;
import java.util.List;

import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.internal.AssumptionViolatedException;
import org.junit.rules.TestName;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;

/**
 * This test class is very similar to {@link TestMethodRuleTest}. If you add a test here, then it is likely that the other will have to be changed.
 * This tests {@link Rule}s attached to fields.
 * {@link TestMethodRuleTest} tests {@link Rule}s attached to methods.
 */
public class TestRuleTest {
    private static boolean wasRun;

    public static class ExampleTest {
        @Rule
        public TestRule example = new TestRule() {
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        wasRun = true;
                        base.evaluate();
                    }

                    ;
                };
            }
        };

        @Test
        public void nothing() {

        }
    }

    @Test
    public void ruleIsIntroducedAndEvaluated() {
        wasRun = false;
        JUnitCore.runClasses(ExampleTest.class);
        assertTrue(wasRun);
    }

    @SuppressWarnings("deprecation")
    public static class BothKindsOfRule implements TestRule, org.junit.rules.MethodRule {
        public int applications = 0;

        public Statement apply(Statement base, FrameworkMethod method,
                Object target) {
            applications++;
            return base;
        }

        public Statement apply(Statement base, Description description) {
            applications++;
            return base;
        }
    }

    public static class OneFieldTwoKindsOfRule {
        @Rule
        public BothKindsOfRule both = new BothKindsOfRule();

        @Test
        public void onlyOnce() {
            assertEquals(1, both.applications);
        }
    }


    @Test
    public void onlyApplyOnceEvenIfImplementsBothInterfaces() {
        assertTrue(JUnitCore.runClasses(OneFieldTwoKindsOfRule.class).wasSuccessful());
    }

    public static class SonOfExampleTest extends ExampleTest {

    }

    @Test
    public void ruleIsIntroducedAndEvaluatedOnSubclass() {
        wasRun = false;
        JUnitCore.runClasses(SonOfExampleTest.class);
        assertTrue(wasRun);
    }

    private static int runCount;

    public static class MultipleRuleTest {
        private static class Increment implements TestRule {
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        runCount++;
                        base.evaluate();
                    }

                    ;
                };
            }
        }

        @Rule
        public TestRule incrementor1 = new Increment();

        @Rule
        public TestRule incrementor2 = new Increment();

        @Test
        public void nothing() {

        }
    }

    @Test
    public void multipleRulesAreRun() {
        runCount = 0;
        JUnitCore.runClasses(MultipleRuleTest.class);
        assertEquals(2, runCount);
    }

    public static class NoRulesTest {
        public int x;

        @Test
        public void nothing() {

        }
    }

    @Test
    public void ignoreNonRules() {
        Result result = JUnitCore.runClasses(NoRulesTest.class);
        assertEquals(0, result.getFailureCount());
    }

    private static String log;

    public static class OnFailureTest {
        @Rule
        public TestRule watcher = new TestWatcher() {
            @Override
            protected void failed(Throwable e, Description description) {
                log += description + " " + e.getClass().getSimpleName();
            }
        };

        @Test
        public void nothing() {
            fail();
        }
    }

    @Test
    public void onFailure() {
        log = "";
        Result result = JUnitCore.runClasses(OnFailureTest.class);
        assertEquals(String.format("nothing(%s) AssertionError", OnFailureTest.class.getName()), log);
        assertEquals(1, result.getFailureCount());
    }

    public static class WatchmanTest {
        private static String watchedLog;

        @Rule
        public TestRule watcher = new TestWatcher() {
            @Override
            protected void failed(Throwable e, Description description) {
                watchedLog += description + " "
                        + e.getClass().getSimpleName() + "\n";
            }

            @Override
            protected void succeeded(Description description) {
                watchedLog += description + " " + "success!\n";
            }
        };

        @Test
        public void fails() {
            fail();
        }

        @Test
        public void succeeds() {
        }
    }

    @Test
    public void succeeded() {
        WatchmanTest.watchedLog = "";
        JUnitCore.runClasses(WatchmanTest.class);
        assertThat(WatchmanTest.watchedLog, containsString(String.format("fails(%s) AssertionError", WatchmanTest.class.getName())));
        assertThat(WatchmanTest.watchedLog, containsString(String.format("succeeds(%s) success!", WatchmanTest.class.getName())));
    }

    public static class BeforesAndAfters {
        private static StringBuilder watchedLog = new StringBuilder();

        @Before
        public void before() {
            watchedLog.append("before ");
        }

        @Rule
        public TestRule watcher = new LoggingTestWatcher(watchedLog);

        @After
        public void after() {
            watchedLog.append("after ");
        }

        @Test
        public void succeeds() {
            watchedLog.append("test ");
        }
    }

    @Test
    public void beforesAndAfters() {
        BeforesAndAfters.watchedLog = new StringBuilder();
        JUnitCore.runClasses(BeforesAndAfters.class);
        assertThat(BeforesAndAfters.watchedLog.toString(),
                is("starting before test after succeeded finished "));
    }

    public static class WrongTypedField {
        @Rule
        public int x = 5;

        @Test
        public void foo() {
        }
    }

    @Test
    public void validateWrongTypedField() {
        assertThat(testResult(WrongTypedField.class),
                hasSingleFailureContaining("must implement MethodRule"));
    }

    public static class SonOfWrongTypedField extends WrongTypedField {

    }

    @Test
    public void validateWrongTypedFieldInSuperclass() {
        assertThat(testResult(SonOfWrongTypedField.class),
                hasSingleFailureContaining("must implement MethodRule"));
    }

    public static class PrivateRule {
        @SuppressWarnings("unused")
        @Rule
        private TestRule rule = new TestName();

        @Test
        public void foo() {
        }
    }

    @Test
    public void validatePrivateRule() {
        assertThat(testResult(PrivateRule.class),
                hasSingleFailureContaining("must be public"));
    }

    public static class CustomTestName implements TestRule {
        public String name = null;

        public Statement apply(final Statement base, final Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    name = description.getMethodName();
                    base.evaluate();
                }
            };
        }
    }

    public static class UsesCustomMethodRule {
        @Rule
        public CustomTestName counter = new CustomTestName();

        @Test
        public void foo() {
            assertEquals("foo", counter.name);
        }
    }

    @Test
    public void useCustomMethodRule() {
        assertThat(testResult(UsesCustomMethodRule.class), isSuccessful());
    }

    public static class MethodExampleTest {
        private TestRule example = new TestRule() {
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        wasRun = true;
                        base.evaluate();
                    }

                    ;
                };
            }
        };

        @Rule
        public TestRule getExample() {
            return example;
        }

        @Test
        public void nothing() {

        }
    }

    @Test
    public void methodRuleIsIntroducedAndEvaluated() {
        wasRun = false;
        JUnitCore.runClasses(MethodExampleTest.class);
        assertTrue(wasRun);
    }

    @SuppressWarnings("deprecation")
    public static class MethodBothKindsOfRule implements TestRule, org.junit.rules.MethodRule {
        public int applications = 0;

        public Statement apply(Statement base, FrameworkMethod method,
                Object target) {
            applications++;
            return base;
        }

        public Statement apply(Statement base, Description description) {
            applications++;
            return base;
        }
    }

    public static class MethodOneFieldTwoKindsOfRule {
        private MethodBothKindsOfRule both = new MethodBothKindsOfRule();

        @Rule
        public MethodBothKindsOfRule getBoth() {
            return both;
        }

        @Test
        public void onlyOnce() {
            assertEquals(1, both.applications);
        }
    }


    @Test
    public void methodOnlyApplyOnceEvenIfImplementsBothInterfaces() {
        assertTrue(JUnitCore.runClasses(MethodOneFieldTwoKindsOfRule.class).wasSuccessful());
    }

    public static class MethodSonOfExampleTest extends MethodExampleTest {

    }

    @Test
    public void methodRuleIsIntroducedAndEvaluatedOnSubclass() {
        wasRun = false;
        JUnitCore.runClasses(MethodSonOfExampleTest.class);
        assertTrue(wasRun);
    }

//	private static int runCount;

    public static class MethodMultipleRuleTest {
        private static class Increment implements TestRule {
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        runCount++;
                        base.evaluate();
                    }

                    ;
                };
            }
        }

        private TestRule incrementor1 = new Increment();

        @Rule
        public TestRule getIncrementor1() {
            return incrementor1;
        }

        private TestRule incrementor2 = new Increment();

        @Rule
        public TestRule getIncrementor2() {
            return incrementor2;
        }

        @Test
        public void nothing() {

        }
    }

    @Test
    public void methodMultipleRulesAreRun() {
        runCount = 0;
        JUnitCore.runClasses(MethodMultipleRuleTest.class);
        assertEquals(2, runCount);
    }

    public static class MethodNoRulesTest {
        public int x;

        @Test
        public void nothing() {

        }
    }

    @Test
    public void methodIgnoreNonRules() {
        Result result = JUnitCore.runClasses(MethodNoRulesTest.class);
        assertEquals(0, result.getFailureCount());
    }

    public static class MethodOnFailureTest {
        private TestRule watchman = new TestWatcher() {
            @Override
            protected void failed(Throwable e, Description description) {
                log += description + " " + e.getClass().getSimpleName();
            }
        };

        @Rule
        public TestRule getWatchman() {
            return watchman;
        }

        @Test
        public void nothing() {
            fail();
        }
    }

    @Test
    public void methodOnFailure() {
        log = "";
        Result result = JUnitCore.runClasses(MethodOnFailureTest.class);
        assertEquals(String.format("nothing(%s) AssertionError", MethodOnFailureTest.class.getName()), log);
        assertEquals(1, result.getFailureCount());
    }

    public static class MethodOnSkippedTest {
        private TestRule watchman = new TestWatcher() {
            @Override
            protected void skipped(AssumptionViolatedException e, Description description) {
                log += description + " " + e.getClass().getSimpleName();
            }
        };

        @Rule
        public TestRule getWatchman() {
            return watchman;
        }

        @Test
        public void nothing() {
            Assume.assumeTrue(false);
        }
    }

    @Test
    public void methodOnSkipped() {
        log = "";
        Result result = JUnitCore.runClasses(MethodOnSkippedTest.class);
        assertEquals(String.format("nothing(%s) AssumptionViolatedException", MethodOnSkippedTest.class.getName()), log);
        assertEquals(0, result.getFailureCount());
        assertEquals(1, result.getRunCount());
    }

    public static class MethodWatchmanTest {
        private static String watchedLog;

        private TestRule watchman = new TestWatcher() {
            @Override
            protected void failed(Throwable e, Description description) {
                watchedLog += description + " "
                        + e.getClass().getSimpleName() + "\n";
            }

            @Override
            protected void succeeded(Description description) {
                watchedLog += description + " " + "success!\n";
            }
        };

        @Rule
        public TestRule getWatchman() {
            return watchman;
        }

        @Test
        public void fails() {
            fail();
        }

        @Test
        public void succeeds() {
        }
    }

    @Test
    public void methodSucceeded() {
        WatchmanTest.watchedLog = "";
        JUnitCore.runClasses(WatchmanTest.class);
        assertThat(WatchmanTest.watchedLog, containsString(String.format("fails(%s) AssertionError", WatchmanTest.class.getName())));
        assertThat(WatchmanTest.watchedLog, containsString(String.format("succeeds(%s) success!", WatchmanTest.class.getName())));
    }

    public static class MethodBeforesAndAfters {
        private static String watchedLog;

        @Before
        public void before() {
            watchedLog += "before ";
        }

        private TestRule watchman = new TestWatcher() {
            @Override
            protected void starting(Description d) {
                watchedLog += "starting ";
            }

            @Override
            protected void finished(Description d) {
                watchedLog += "finished ";
            }

            @Override
            protected void succeeded(Description d) {
                watchedLog += "succeeded ";
            }
        };

        @Rule
        public TestRule getWatchman() {
            return watchman;
        }

        @After
        public void after() {
            watchedLog += "after ";
        }

        @Test
        public void succeeds() {
            watchedLog += "test ";
        }
    }

    @Test
    public void methodBeforesAndAfters() {
        MethodBeforesAndAfters.watchedLog = "";
        JUnitCore.runClasses(MethodBeforesAndAfters.class);
        assertThat(MethodBeforesAndAfters.watchedLog, is("starting before test after succeeded finished "));
    }

    public static class MethodWrongTypedField {
        @Rule
        public int getX() {
            return 5;
        }

        @Test
        public void foo() {
        }
    }

    @Test
    public void methodValidateWrongTypedField() {
        assertThat(testResult(MethodWrongTypedField.class),
                hasSingleFailureContaining("must return an implementation of MethodRule"));
    }

    public static class MethodSonOfWrongTypedField extends MethodWrongTypedField {

    }

    @Test
    public void methodValidateWrongTypedFieldInSuperclass() {
        assertThat(testResult(MethodSonOfWrongTypedField.class),
                hasSingleFailureContaining("must return an implementation of MethodRule"));
    }

    public static class MethodPrivateRule {
        @SuppressWarnings("unused")
        @Rule
        private TestRule getRule() {
            return new TestName();
        }

        @Test
        public void foo() {
        }
    }

    @Test
    public void methodValidatePrivateRule() {
        assertThat(testResult(MethodPrivateRule.class),
                hasSingleFailureContaining("must be public"));
    }

    public static class MethodUsesCustomMethodRule {
        private CustomTestName counter = new CustomTestName();

        @Rule
        public CustomTestName getCounter() {
            return counter;
        }

        @Test
        public void foo() {
            assertEquals("foo", counter.name);
        }
    }

    @Test
    public void methodUseCustomMethodRule() {
        assertThat(testResult(MethodUsesCustomMethodRule.class), isSuccessful());
    }

    private static final List<String> orderList = new LinkedList<String>();

    private static class OrderTestRule implements TestRule {
        private String name;

        public OrderTestRule(String name) {
            this.name = name;
        }

        public Statement apply(final Statement base, final Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    orderList.add(name);
                    base.evaluate();
                }
            };
        }
    }

    ;

    public static class UsesFieldAndMethodRule {
        @Rule
        public OrderTestRule orderMethod() {
            return new OrderTestRule("orderMethod");
        }

        @Rule
        public OrderTestRule orderField = new OrderTestRule("orderField");

        @Test
        public void foo() {
            assertEquals("orderField", orderList.get(0));
            assertEquals("orderMethod", orderList.get(1));
        }
    }

    @Test
    public void usesFieldAndMethodRule() {
        orderList.clear();
        assertThat(testResult(UsesFieldAndMethodRule.class), isSuccessful());
    }

    public static class MultipleCallsTest implements TestRule {
        public int applications = 0;

        public Statement apply(Statement base, Description description) {
            applications++;
            return base;
        }
    }

    public static class CallMethodOnlyOnceRule {
        int countOfMethodCalls = 0;

        private static class Dummy implements TestRule {
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        base.evaluate();
                    }

                    ;
                };
            }
        }

        @Rule
        public Dummy both() {
            countOfMethodCalls++;
            return new Dummy();
        }

        @Test
        public void onlyOnce() {
            assertEquals(1, countOfMethodCalls);
        }
    }

    @Test
    public void testCallMethodOnlyOnceRule() {
        assertTrue(JUnitCore.runClasses(CallMethodOnlyOnceRule.class).wasSuccessful());
    }
}
