package be.iminds.ilabt.jfed.testing.base;

import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticAggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Objects;

/**
 * ApiTestFactory
 */
public class ApiTestFactory {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(ApiTestFactory.class);

    protected final Injector injector;
    protected final GeniUserProvider geniUserProvider;

    protected final JFedConnectionProvider connectionProvider;
    protected final TestbedInfoSource testbedInfoSource;
    protected final JFedPreferences jFedPreferences;
    protected final AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory
            automaticAggregateManagerWrapperFactory;

    @Inject
    public ApiTestFactory(Injector injector, GeniUserProvider geniUserProvider,
                          JFedConnectionProvider connectionProvider, TestbedInfoSource testbedInfoSource,
                          JFedPreferences jFedPreferences,
                          AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory automaticAggregateManagerWrapperFactory) {
        this.injector = injector;
        this.geniUserProvider = geniUserProvider;
        this.connectionProvider = connectionProvider;
        this.testbedInfoSource = testbedInfoSource;
        this.jFedPreferences = jFedPreferences;
        this.automaticAggregateManagerWrapperFactory = automaticAggregateManagerWrapperFactory;
    }

    public ApiTest createApiTest(Logger logger, TargetAuthority testedAuthority, String testClassName) throws InvalidApiTestException {
        try {
            Class<?> foundClass = Class.forName(testClassName);
            if (!ApiTest.class.isAssignableFrom(foundClass)) {
                throw new InvalidApiTestException("The test class \"" + testClassName + "\" is not an ApiTest");
            }
            @SuppressWarnings("unchecked")
            Class<? extends ApiTest> testClass = (Class<? extends ApiTest>) foundClass;
            return createApiTest(logger, testedAuthority, testClass);
        } catch (ClassNotFoundException e) {
            throw new InvalidApiTestException("The test class \"" + testClassName + "\" cannot be found", e);
        }
    }

    /**
     * create an ApiTest with nulls filled in. This is useful to get some pseudo static methods.
     */
    public ApiTest createNullApiTest(Class<? extends ApiTest> testClass) throws InvalidApiTestException {
        Constructor[] constructors = testClass.getConstructors();
        for (Constructor c : constructors) {
            if (c.getAnnotation(javax.inject.Inject.class) != null) {
                Object[] params = new Object[c.getParameterCount()];

                try {
                    return (ApiTest) c.newInstance(params);
                } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
                    throw new InvalidApiTestException("The class \"" + testClass.getName() + "\" is not a valid ApiTest", e);
                }
            }
        }
        throw new InvalidApiTestException("The class \"" + testClass.getName() + "\" is not a valid ApiTest");
    }

    public ApiTest createApiTest(Logger logger, final TargetAuthority testedAuthority, Class<? extends ApiTest> testClass) throws InvalidApiTestException {

        //all constructors must take these arguments:
        //Logger logger, TargetAuthority testedAuthority, GeniUser user, JFedConnectionProvider connectionProvider, TestbedInfoSource testbedInfoSource

        //and optionally: AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory automaticAggregateManagerWrapperFactory

        try {
            ApiTest test = null;

            Constructor[] constructors = testClass.getConstructors();
            for (Constructor c : constructors) {
                if (c.getParameterCount() == 6) {
                    assert test == null;
                    Constructor<? extends ApiTest> constructor = testClass.getConstructor(
                            Logger.class,
                            TargetAuthority.class,
                            GeniUserProvider.class,
                            JFedConnectionProvider.class,
                            TestbedInfoSource.class,
                            JFedPreferences.class);

                    test = constructor.newInstance(
                            logger,
                            testedAuthority,
                            geniUserProvider,
                            connectionProvider,
                            testbedInfoSource,
                            jFedPreferences);
                }
                if (c.getParameterCount() == 7) {
                    assert test == null;
                    Constructor<? extends ApiTest> constructor = testClass.getConstructor(
                            Logger.class,
                            TargetAuthority.class,
                            GeniUser.class,
                            JFedConnectionProvider.class,
                            TestbedInfoSource.class,
                            JFedPreferences.class,
                            AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory.class);

                    test = constructor.newInstance(
                            logger,
                            testedAuthority,
                            geniUserProvider,
                            connectionProvider,
                            testbedInfoSource,
                            jFedPreferences,
                            automaticAggregateManagerWrapperFactory);
                }
            }

            if (test == null && injector != null) {
                boolean hasTargetBinding;
                TargetAuthority target = null;
                try {
                    target = injector.getInstance(TargetAuthority.class);
                    hasTargetBinding = true;
                } catch (Exception e) {
                    if (!(e instanceof com.google.inject.ConfigurationException))
                        LOG.warn("Injector did not already have target binding, and returned unexpected exception class.", e);
                    hasTargetBinding = false;
                }
                if (testedAuthority != null && target != null) {
                    assert hasTargetBinding;
                    if (!Objects.equals(target.getOrigUrn(), testedAuthority.getOrigUrn()))
                        throw new RuntimeException(
                                "Internal error: the injector was already bound for another authority! " +
                                        "already bound to: " + testedAuthority.getOrigUrn() + " new target: " + target.getOrigUrn());
                }

                if (!hasTargetBinding) {
                    LOG.info("Constructing " + testClass.getName() + " using Guice child injector to bind to target");
                    Injector usedInjector = injector.createChildInjector(new AbstractModule() {
                        @Override
                        protected void configure() {
                            bind(TargetAuthority.class).toInstance(testedAuthority);
                        }
                    });
                    test = usedInjector.getInstance(testClass);
                } else {
                    LOG.info("Constructing " + testClass.getName() + " using Guice injector (already bound to target)");
                    test = injector.getInstance(testClass);
                }
            }

            if (test == null) {
                throw new RuntimeException("Test class \"" + testClass.getName() + "\" has no suitable constructor");
            }

            return test;
        } catch (NoSuchMethodException e) {
            LOG.error("ApiTest subclass constructor not found. Available constructors: " + Arrays.asList(testClass.getConstructors()));
            throw new InvalidApiTestException("The class \"" + testClass.getName() + "\" is not a valid ApiTest", e);
        } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
            throw new InvalidApiTestException("The class \"" + testClass.getName() + "\" is not a valid ApiTest", e);
        }
    }

    public class InvalidApiTestException extends Exception {
        public InvalidApiTestException() {
        }

        public InvalidApiTestException(String message) {
            super(message);
        }

        public InvalidApiTestException(String message, Throwable cause) {
            super(message, cause);
        }

        public InvalidApiTestException(Throwable cause) {
            super(cause);
        }

        public InvalidApiTestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
}
