package org.testfun.jee;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.hamcrest.*;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Assert;
import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.mockito.ArgumentMatcher;
import javax.validation.ConstraintViolationException;

public class ExpectedConstraintViolation implements MethodRule {

    /**
     * @return a Rule that expects no violation to be thrown
     * (identical to behavior without this Rule)
     */
    public static ExpectedConstraintViolation none() {
        return new ExpectedConstraintViolation();
    }

    private Matcher<Object> matcher;

    private ExpectedConstraintViolation() { }

    public Statement apply(Statement base, FrameworkMethod method, Object target) {
        return new ExpectedExceptionStatement(base);
    }

    /**
     * Adds {@code matcher} to the list of requirements for any thrown exception.
     */
    // Should be able to remove this suppression in some brave new hamcrest world.
    @SuppressWarnings("unchecked")
    public void expect(Matcher<?> matcher) {
        if (this.matcher == null) {
            this.matcher = (Matcher<Object>) matcher;
        } else {
            this.matcher = CombinableMatcher.both(this.matcher).and((Matcher<Object>) matcher);
        }
    }

    /**
     * Adds to the list of requirements for any thrown exception that it
     * should <em>contain</em> string {@code substring}
     */
    public void expectViolation(String substring) {
        if (matcher == null) {
            expect(CombinableMatcher.either(
                    new CausedBy(org.hibernate.exception.ConstraintViolationException.class))
                    .or(new CausedBy(ConstraintViolationException.class)));
        }

        expectMessage(CoreMatchers.containsString(substring));
    }

    /**
     * Adds {@code matcher} to the list of requirements for the message
     * returned from any thrown exception.
     */
    public void expectMessage(Matcher<String> matcher) {
        expect(matcherHasMessage(matcher));
    }

    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) {
                if (matcher == null) {
                    throw e;
                }
                if (e.getCause() != null) e = e.getCause();
                Assert.assertThat(e, matcher);
                return;
            }
            if (matcher != null) {
                throw new AssertionError("Expected test to throw "
                        + StringDescription.toString(matcher));
            }
        }
    }

    private Matcher<Throwable> matcherHasMessage(final Matcher<String> matcher) {
        return new TypeSafeMatcher<Throwable>() {
            public void describeTo(Description description) {
                description.appendText("violation with message ");
                description.appendDescriptionOf(matcher);
            }

            @Override
            public boolean matchesSafely(Throwable item) {
                return matcher.matches(item.getMessage());
            }
        };
    }

    class CausedBy extends ArgumentMatcher<Throwable> {

        private final Class<? extends Throwable> throwableClass;

        public CausedBy(Class<? extends Throwable> throwableClass) {
            this.throwableClass = throwableClass;
        }

        public boolean matches(Object argument) {
            return ExceptionUtils.indexOfThrowable((Throwable) argument, throwableClass) >= 0;
        }

        public void describeTo(Description description) {
            description.appendText("isCausedBy(" + throwableClass.getName() + ")");
        }

    }

}