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

import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

/**
 * Base class to simplify building {@code TestExecutionListener}s which process the fields on the test class to apply
 * some behaviour to them based on the presence of an annotation.
 */
public abstract class AbstractAnnotationTestExecutionListener<T extends Annotation>
        extends AbstractTestExecutionListener {
    
    private final Class<T> annotationClass;

    protected AbstractAnnotationTestExecutionListener(Class<T> annotationClass) {
        this.annotationClass = annotationClass;
    }

    @Override
    public void prepareTestInstance(TestContext testContext) {
        Class<?> testClass = testContext.getTestClass();

        ReflectionUtils.doWithFields(testClass, field -> {
            try {
                T annotation = getAnnotation(field, annotationClass);
                if (annotation != null) {
                    validateAnnotation(field, annotation);
                    applyAnnotation(testContext, field, annotation);
                }
            } catch (Exception e) {
                throw new IllegalStateException("Failed to read annotation on " + field, e);
            }
        });
    }
    
    protected void applyAnnotation(TestContext testContext, Field field, T annotation) throws Exception {
        Object value = createValue(field, annotation);
        
        field.setAccessible(true);
        field.set(testContext.getTestInstance(), value);
    }
    
    protected abstract Object createValue(Field field, T annotation) throws Exception;
    
    protected T getAnnotation(Field field, Class<T> annotationClass) {
        return field.getAnnotation(annotationClass);
    }
    
    protected void validateAnnotation(Field field, T annotation) {
        //No-op
    }
}
