package org.switchyard.component.camel.deploy;

import static org.junit.matchers.JUnitMatchers.containsString;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.internal.matchers.TypeSafeMatcher;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.runners.model.Statement;
import org.switchyard.HandlerException;
import org.switchyard.test.InvocationFaultException;

public class SwitchYardExpectedException implements TestRule {

    private final ExpectedException _delegate = ExpectedException.none();

    private SwitchYardExpectedException() {
    }

    public static SwitchYardExpectedException none() {
        return new SwitchYardExpectedException();
    }

    public Statement apply(Statement base, org.junit.runner.Description desc) {
        return _delegate.apply(base, desc);
    }

    public void expect(Class<? extends Throwable> type) {
        _delegate.expect(new ExceptionCauseMatcher(type));
    }

    public void expectMessage(String message) {
        expectMessage(containsString(message));
    }

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

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

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

    private class ExceptionCauseMatcher extends BaseMatcher<Throwable> {

        private Class<? extends Throwable> expectedClass;

        public ExceptionCauseMatcher(final Class<? extends Throwable> expectedClass) {
            this.expectedClass = expectedClass;
        }

        @Override
        public void describeTo(final Description description) {
            description.appendText("Exception.getCause() should be [" + expectedClass.toString() + "]");
        }

        @Override
        public boolean matches(final Object obj) {
            if (!(obj instanceof Exception)) {
                return false;
            }

            final Exception e = (Exception) obj;
            final Throwable thrown = getCauseFromHandlerException(e);
            return expectedClass.equals(thrown.getClass());
        }

    }

    public static Throwable getCauseFromHandlerException(final Throwable thrown) {
        Throwable cause = thrown;
        if (thrown instanceof InvocationFaultException) {
            final InvocationFaultException faultException = (InvocationFaultException) thrown;
            if (faultException.getCause() instanceof HandlerException) {
                HandlerException handlerEx = (HandlerException)faultException.getCause();
                cause = handlerEx.isWrapper() ? handlerEx.getCause() : handlerEx;
            } else if (faultException.getFaultMessage().getContent() instanceof Exception) {
                cause = faultException.getFaultMessage().getContent(Throwable.class);
            }
        }
        return cause;
    }

}