package com.atlassian.bitbucket.scm.context;

import com.atlassian.bitbucket.scm.annotation.DirtiesRepo;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.repository.Repository;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Applies the {@link DirtiesRepo} annotation during test execution.
 */
public class DirtiesRepoTestExecutionListener extends AbstractTestExecutionListener {

    private static final String[] ALL_REPOSITORIES = (String[]) AnnotationUtils.getDefaultValue(DirtiesRepo.class);

    @Autowired
    private RepositoryCache repositoryCache;

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        Method testMethod = testContext.getTestMethod();

        DirtiesRepo annotation = testMethod.getAnnotation(DirtiesRepo.class);
        if (annotation != null) {
            cleanRepositories(testContext, annotation);
        }
    }

    private void cleanRepositories(TestContext testContext, DirtiesRepo annotation) throws IllegalAccessException {
        Set<String> dirtiedRepositories = ImmutableSet.copyOf(annotation.value());
        boolean all = Arrays.equals(ALL_REPOSITORIES, annotation.value());

        //This test method dirties one or more repositories. The annotation will tell us which ones, but we need
        //to look at the class's fields to determine
        Map<String, Repository> repositoriesByField = mapRepositories(testContext);
        for (Map.Entry<String, Repository> entry : repositoriesByField.entrySet()) {
            //Once we have the repositories used by the test mapped by field name, the next step is to determine
            //which are now dirty. By default, the annotation marks all repositories as dirty, but to reduce the
            //workload specific repositories may be marked instead. So we check for both. Either all, if the value
            //of the annotation matches the default, or selected if the field name matches a repository referenced
            //specifically in the annotation's value.
            if (all || dirtiedRepositories.contains(entry.getKey())) {
                //The name of the repository is the location that was unzipped when the repository was created,
                //so we leverage that fact here to simplify removing the repository from the cache.
                repositoryCache.removeRepository(entry.getValue().getName());
            }
        }
    }

    private Map<String, Repository> mapRepositories(TestContext testContext) throws IllegalAccessException {
        Class<?> testClass = testContext.getTestClass();
        Field[] fields = FieldUtils.getAllFields(testClass);
        
        Map<String, Repository> map = new HashMap<>(fields.length);
        for (Field field : fields) {
            if (field.isAnnotationPresent(Repo.class)) {
                field.setAccessible(true);
                
                map.put(field.getName(), (Repository) field.get(testContext.getTestInstance()));
            }
        }
        return map;
    }
}
