package org.junit.rules;

import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.internal.matchers.ThrowableCauseMatcher.hasCause;
import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage;

import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import org.junit.AssumptionViolatedException;
import org.junit.runners.model.Statement;

/**
 * The {@code ExpectedException} rule allows you to verify that your code
 * throws a specific exception.
 *
 * <h3>Usage</h3>
 *
 * <pre> public class SimpleExpectedExceptionTest {
 *     &#064;Rule
 *     public ExpectedException thrown= ExpectedException.none();
 *
 *     &#064;Test
 *     public void throwsNothing() {
 *         // no exception expected, none thrown: passes.
 *     }
 *
 *     &#064;Test
 *     public void throwsExceptionWithSpecificType() {
 *         thrown.expect(NullPointerException.class);
 *         throw new NullPointerException();
 *     }
 * }</pre>
 * 
 * <p>
 * You have to add the {@code ExpectedException} rule to your test.
 * This doesn't affect your existing tests (see {@code throwsNothing()}).
 * After specifiying the type of the expected exception your test is
 * successful when such an exception is thrown and it fails if a
 * different or no exception is thrown.
 *
 * <p>
 * Instead of specifying the exception's type you can characterize the
 * expected exception based on other criterias, too:
 *
 * <ul>
 *   <li>The exception's message contains a specific text: {@link #expectMessage(String)}</li>
 *   <li>The exception's message complies with a Hamcrest matcher: {@link #expectMessage(Matcher)}</li>
 *   <li>The exception's cause complies with a Hamcrest matcher: {@link #expectCause(Matcher)}</li>
 *   <li>The exception itself complies with a Hamcrest matcher: {@link #expect(Matcher)}</li>
 * </ul>
 *
 * <p>
 * You can combine any of the presented expect-methods. The test is
 * successful if all specifications are met.
 * <pre> &#064;Test
 * public void throwsException() {
 *     thrown.expect(NullPointerException.class);
 *     thrown.expectMessage(&quot;happened&quot;);
 *     throw new NullPointerException(&quot;What happened?&quot;);
 * }</pre>
 *
 * <h3>AssumptionViolatedExceptions</h3>
 * <p>
 * JUnit uses {@link AssumptionViolatedException}s for indicating that a test
 * provides no useful information. (See {@link org.junit.Assume} for more
 * information.) You have to call {@code assume} methods before you set
 * expectations of the {@code ExpectedException} rule. In this case the rule
 * will not handle consume the exceptions and it can be handled by the
 * framework. E.g. the following test is ignored by JUnit's default runner.
 *
 * <pre> &#064;Test
 * public void ignoredBecauseOfFailedAssumption() {
 *     assumeTrue(false); // throws AssumptionViolatedException
 *     thrown.expect(NullPointerException.class);
 * }</pre>
 *
 * <h3>AssertionErrors</h3>
 *
 * <p>
 * JUnit uses {@link AssertionError}s for indicating that a test is failing. You
 * have to call {@code assert} methods before you set expectations of the
 * {@code ExpectedException} rule, if they should be handled by the framework.
 * E.g. the following test fails because of the {@code assertTrue} statement.
 *
 * <pre> &#064;Test
 * public void throwsUnhandled() {
 *     assertTrue(false); // throws AssertionError
 *     thrown.expect(NullPointerException.class);
 * }</pre>
 *
 * <h3>Missing Exceptions</h3>
 * <p>
 * By default missing exceptions are reported with an error message
 * like "Expected test to throw an instance of foo". You can configure a different
 * message by means of {@link #reportMissingExceptionWithMessage(String)}. You
 * can use a {@code %s} placeholder for the description of the expected
 * exception. E.g. "Test doesn't throw %s." will fail with the error message
 * "Test doesn't throw an instance of foo.".
 *
 * @since 4.7
 */
public class ExpectedException implements TestRule {
    /**
     * Returns a {@linkplain TestRule rule} that expects no exception to
     * be thrown (identical to behavior without this rule).
     */
    public static ExpectedException none() {
        return new ExpectedException();
    }

    private final ExpectedExceptionMatcherBuilder matcherBuilder = new ExpectedExceptionMatcherBuilder();

    private String missingExceptionMessage= "Expected test to throw %s";

    private ExpectedException() {
    }

    /**
     * This method does nothing. Don't use it.
     * @deprecated AssertionErrors are handled by default since JUnit 4.12. Just
     *             like in JUnit &lt;= 4.10.
     */
    @Deprecated
    public ExpectedException handleAssertionErrors() {
        return this;
    }

    /**
     * This method does nothing. Don't use it.
     * @deprecated AssumptionViolatedExceptions are handled by default since
     *             JUnit 4.12. Just like in JUnit &lt;= 4.10.
     */
    @Deprecated
    public ExpectedException handleAssumptionViolatedExceptions() {
        return this;
    }

    /**
     * Specifies the failure message for tests that are expected to throw 
     * an exception but do not throw any. You can use a {@code %s} placeholder for
     * the description of the expected exception. E.g. "Test doesn't throw %s."
     * will fail with the error message
     * "Test doesn't throw an instance of foo.".
     *
     * @param message exception detail message
     * @return the rule itself
     */
    public ExpectedException reportMissingExceptionWithMessage(String message) {
        missingExceptionMessage = message;
        return this;
    }

    public Statement apply(Statement base,
            org.junit.runner.Description description) {
        return new ExpectedExceptionStatement(base);
    }

    /**
     * Verify that your code throws an exception that is matched by
     * a Hamcrest matcher.
     * <pre> &#064;Test
     * public void throwsExceptionThatCompliesWithMatcher() {
     *     NullPointerException e = new NullPointerException();
     *     thrown.expect(is(e));
     *     throw e;
     * }</pre>
     */
    public void expect(Matcher<?> matcher) {
        matcherBuilder.add(matcher);
    }

    /**
     * Verify that your code throws an exception that is an
     * instance of specific {@code type}.
     * <pre> &#064;Test
     * public void throwsExceptionWithSpecificType() {
     *     thrown.expect(NullPointerException.class);
     *     throw new NullPointerException();
     * }</pre>
     */
    public void expect(Class<? extends Throwable> type) {
        expect(instanceOf(type));
    }

    /**
     * Verify that your code throws an exception whose message contains
     * a specific text.
     * <pre> &#064;Test
     * public void throwsExceptionWhoseMessageContainsSpecificText() {
     *     thrown.expectMessage(&quot;happened&quot;);
     *     throw new NullPointerException(&quot;What happened?&quot;);
     * }</pre>
     */
    public void expectMessage(String substring) {
        expectMessage(containsString(substring));
    }

    /**
     * Verify that your code throws an exception whose message is matched 
     * by a Hamcrest matcher.
     * <pre> &#064;Test
     * public void throwsExceptionWhoseMessageCompliesWithMatcher() {
     *     thrown.expectMessage(startsWith(&quot;What&quot;));
     *     throw new NullPointerException(&quot;What happened?&quot;);
     * }</pre>
     */
    public void expectMessage(Matcher<String> matcher) {
        expect(hasMessage(matcher));
    }

    /**
     * Verify that your code throws an exception whose cause is matched by 
     * a Hamcrest matcher.
     * <pre> &#064;Test
     * public void throwsExceptionWhoseCauseCompliesWithMatcher() {
     *     NullPointerException expectedCause = new NullPointerException();
     *     thrown.expectCause(is(expectedCause));
     *     throw new IllegalArgumentException(&quot;What happened?&quot;, cause);
     * }</pre>
     */
    public void expectCause(Matcher<? extends Throwable> expectedCause) {
        expect(hasCause(expectedCause));
    }

    private class ExpectedExceptionStatement extends Statement {
        private final Statement next;

        public ExpectedExceptionStatement(Statement base) {
            next = base;
        }

        @Override
        public void evaluate() throws Throwable {
            try {
                next.evaluate();
            } catch (Throwable e) {
                handleException(e);
                return;
            }
            if (isAnyExceptionExpected()) {
                failDueToMissingException();
            }
        }
    }

    private void handleException(Throwable e) throws Throwable {
        if (isAnyExceptionExpected()) {
            assertThat(e, matcherBuilder.build());
        } else {
            throw e;
        }
    }

    private boolean isAnyExceptionExpected() {
        return matcherBuilder.expectsThrowable();
    }

    private void failDueToMissingException() throws AssertionError {
        fail(missingExceptionMessage());
    }
    
    private String missingExceptionMessage() {
        String expectation= StringDescription.toString(matcherBuilder.build());
        return format(missingExceptionMessage, expectation);
    }
}
