package junitparams.internal;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.TestClass;

import junitparams.internal.annotation.FrameworkMethodAnnotations;
import junitparams.internal.parameters.ParametersReader;
import junitparams.naming.MacroSubstitutionNamingStrategy;
import junitparams.naming.TestCaseNamingStrategy;

/**
 * A wrapper for a test method
 *
 * @author Pawel Lipinski
 */
public class TestMethod {
    private FrameworkMethod frameworkMethod;
    private FrameworkMethodAnnotations frameworkMethodAnnotations;
    private Class<?> testClass;
    private ParametersReader parametersReader;
    private Object[] cachedParameters;
    private TestCaseNamingStrategy namingStrategy;
    private DescribableFrameworkMethod describableFrameworkMethod;

    public TestMethod(FrameworkMethod method, TestClass testClass) {
        this.frameworkMethod = method;
        this.testClass = testClass.getJavaClass();
        frameworkMethodAnnotations = new FrameworkMethodAnnotations(method);
        parametersReader = new ParametersReader(testClass(), frameworkMethod);

        namingStrategy = new MacroSubstitutionNamingStrategy(this);
    }

    public String name() {
        return frameworkMethod.getName();
    }

    public static List<FrameworkMethod> listFrom(TestClass testClass) {
        List<FrameworkMethod> methods = new ArrayList<FrameworkMethod>();

        for (FrameworkMethod frameworkMethod : testClass.getAnnotatedMethods(Test.class)) {
            TestMethod testMethod = new TestMethod(frameworkMethod, testClass);
            methods.add(testMethod.describableFrameworkMethod());
        }

        return methods;
    }

    @Override
    public int hashCode() {
        return frameworkMethod.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof TestMethod)
                && hasTheSameNameAsFrameworkMethod((TestMethod) obj)
                && hasTheSameParameterTypesAsFrameworkMethod((TestMethod) obj);
    }

    private boolean hasTheSameNameAsFrameworkMethod(TestMethod testMethod) {
        return frameworkMethod.getName().equals(testMethod.frameworkMethod.getName());
    }

    private boolean hasTheSameParameterTypesAsFrameworkMethod(TestMethod testMethod) {
        Class<?>[] frameworkMethodParameterTypes = frameworkMethod.getMethod().getParameterTypes();
        Class<?>[] testMethodParameterTypes = testMethod.frameworkMethod.getMethod().getParameterTypes();
        return Arrays.equals(frameworkMethodParameterTypes, testMethodParameterTypes);
    }

    private Class<?> testClass() {
        return testClass;
    }

    private boolean isIgnored() {
        return hasIgnoredAnnotation() || hasNoParameters();
    }

    private boolean hasIgnoredAnnotation() {
        return frameworkMethodAnnotations.hasAnnotation(Ignore.class);
    }

    private boolean hasNoParameters() {
       return isParameterised() && parametersSets().length == 0;
    }

    private boolean isNotIgnored() {
        return !isIgnored();
    }

    public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
        return frameworkMethodAnnotations.getAnnotation(annotationType);
    }

    private Description getDescription(Object[] params, int i) {
        Object paramSet = params[i];
        String name = namingStrategy.getTestCaseName(i, paramSet);
        String uniqueMethodId = Utils.uniqueMethodId(i, paramSet, name());

        return Description.createTestDescription(testClass().getName(), name, uniqueMethodId);
    }

    DescribableFrameworkMethod describableFrameworkMethod() {
        if (describableFrameworkMethod == null) {
            Description baseDescription = Description.createTestDescription(
                    testClass, name(), frameworkMethodAnnotations.allAnnotations());
            Method method = frameworkMethod.getMethod();
            try {
                describableFrameworkMethod =
                        createDescribableFrameworkMethod(method, baseDescription);
            } catch (IllegalStateException e) {
                // Defer error until running.
                describableFrameworkMethod =
                        new DeferredErrorFrameworkMethod(method, baseDescription, e);
            }
        }

        return describableFrameworkMethod;
    }

    private DescribableFrameworkMethod createDescribableFrameworkMethod(Method method, Description baseDescription) {
        if (isParameterised()) {
            if (isNotIgnored()) {
                Object[] parametersSets = parametersSets();
                List<InstanceFrameworkMethod> methods
                        = new ArrayList<InstanceFrameworkMethod>();
                for (int i = 0; i < parametersSets.length; i++) {
                    Object parametersSet = parametersSets[i];
                    Description description = getDescription(parametersSets, i);
                    methods.add(new InstanceFrameworkMethod(
                            method, baseDescription.childlessCopy(),
                            description, parametersSet));
                }

                return new ParameterisedFrameworkMethod(method, baseDescription, methods);
            }

            warnIfNoParamsGiven();
        } else {
            verifyMethodCanBeRunByStandardRunner(frameworkMethod);
        }

        // The method to use if it was ignored or was parameterized but had no parameters.
        return new NonParameterisedFrameworkMethod(method, baseDescription, isIgnored());
    }

    private void verifyMethodCanBeRunByStandardRunner(FrameworkMethod method) {
        List<Throwable> errors = new ArrayList<Throwable>();
        method.validatePublicVoidNoArg(false, errors);
        if (!errors.isEmpty()) {
            throw new RuntimeException(errors.get(0));
        }
    }

    public Object[] parametersSets() {
        if (cachedParameters == null) {
            cachedParameters = parametersReader.read();
        }
        return cachedParameters;
    }

    private void warnIfNoParamsGiven() {
        if (isNotIgnored() && isParameterised() && parametersSets().length == 0)
            System.err.println("Method " + name() + " gets empty list of parameters, so it's being ignored!");
    }

    public FrameworkMethod frameworkMethod() {
        return frameworkMethod;
    }

    boolean isParameterised() {
        return frameworkMethodAnnotations.isParametrised();
    }
}