package com.atlassian.bitbucket.mesh.test.context;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

import java.util.Collection;
import java.util.Map;

/**
 * Delegates to one or more {@code TestExecutionListener}s configured in the Spring {@code ApplicationContext}. This
 * allows other listeners to benefit from Spring injection, rather than requiring them to look up beans from the
 * context to prepare their internal state. It also allows both the listeners and their order to be configured in
 * Spring, rather than requiring them to be annotated on the test classes.
 */
public class DelegatingTestExecutionListener implements TestExecutionListener {

    private TestExecutionListener delegate;

    @Override
    public void afterTestClass(TestContext testContext) throws Exception {
        getDelegate(testContext).afterTestClass(testContext);
    }

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        getDelegate(testContext).afterTestMethod(testContext);
    }

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        getDelegate(testContext).beforeTestClass(testContext);
    }

    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        getDelegate(testContext).beforeTestMethod(testContext);
    }

    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        getDelegate(testContext).prepareTestInstance(testContext);
    }

    protected TestExecutionListener getDelegate(TestContext context) {
        if (delegate == null || isDirty(context)) {
            //Find all TestExecutionListeners in the ApplicationContext and create a chain from them if necessary
            ApplicationContext applicationContext;
            try {
                applicationContext = context.getApplicationContext();
            } catch (RuntimeException | Error e) {
                //If the ApplicationContext fails to load for any reason, initialize the delegate with a no-op
                //implementation. This way we don't get duplicate errors from beforeTestClass and afterTestClass
                //both trying to load the delegate and failing
                delegate = new NullTestExecutionListener();

                throw e;
            }

            //Use BeanFactoryUtils to include ancestors. This is important for tests using @ContextHierarchy instead
            //of @ContextConfiguration. Using ApplicationContext.getBeansOfType(TestExecutionListener.class) won't
            //consider any listeners defined in parent contexts
            Map<String, TestExecutionListener> beans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, TestExecutionListener.class);

            Collection<TestExecutionListener> listeners = beans.values();
            if (listeners.size() == 1) {
                //Simple optimization--no point having the extra stack frame if we don't need it
                delegate = listeners.iterator().next();
            } else {
                delegate = new ChainTestExecutionListener(listeners);
            }
        }

        return delegate;
    }

    protected boolean isDirty(TestContext context) {
        //When @DirtiesContext is used, it sets this attribute for the DI listener's benefit. However, because we
        //are parked on top of the listener chain, our methods are guaranteed to be invoked first. That means we
        //have the opportunity to detect the attribute before the DI listener detects and removes it.
        return Boolean.TRUE.equals(context.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE));
    }

    private static class NullTestExecutionListener extends AbstractTestExecutionListener {

        //The base class implements every method as a no-op; exactly what we want here
    }
}
