package com.github.arteam.jdit;

import com.github.arteam.jdit.annotations.DBIHandle;
import com.github.arteam.jdit.annotations.DBIInstance;
import com.github.arteam.jdit.annotations.TestedDao;
import com.github.arteam.jdit.annotations.TestedSqlObject;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * <p>
 * Component which injects test instances (DBI, Handles, SQLObjects, JDBI DAOs)
 * to the fields with corresponding annotations in the test.
 */
class TestObjectsInjector {

    private final Jdbi dbi;
    private final Handle handle;

    TestObjectsInjector(Jdbi dbi, Handle handle) {
        this.dbi = dbi;
        this.handle = handle;
    }

    /**
     * Inject test instances to the test.
     * Search the test instance for field with annotations and inject test objects.
     *
     * @param test current test
     * @throws IllegalAccessException reflection error
     */
    void injectTestedInstances(Object test) throws IllegalAccessException {
        // TODO Cache reflection information
        List<Field> fields = new ArrayList<>();
        Class<?> currentTestClass = test.getClass();
        while (true) {
            Collections.addAll(fields, currentTestClass.getDeclaredFields());
            Class<?> superClass = test.getClass().getSuperclass();
            if (superClass.equals(currentTestClass)) {
                break;
            }
            currentTestClass = superClass;
        }
        for (Field field : fields) {
            Annotation[] annotations = field.getAnnotations();
            if (annotations == null) {
                continue;
            }
            for (Annotation annotation : annotations) {
                if (annotation.annotationType().equals(DBIHandle.class)) {
                    handleDbiHandle(test, field);
                } else if (annotation.annotationType().equals(DBIInstance.class)) {
                    handleDbiInstance(test, field);
                } else if (annotation.annotationType().equals(TestedDao.class)) {
                    handleDbiDao(test, field);
                } else if (annotation.annotationType().equals(TestedSqlObject.class)) {
                    handleDbiSqlObject(test, field);
                }
            }
        }
    }

    /**
     * Inject a DBI handle to a field with {@link DBIHandle} annotation.
     *
     * @param test  current test
     * @param field current field
     * @throws IllegalAccessException reflection error
     */
    private void handleDbiHandle(Object test, Field field) throws IllegalAccessException {
        if (!field.getType().equals(Handle.class)) {
            throw new IllegalArgumentException("Unable inject a DBI handle to a " +
                    "field with type " + field.getType());
        }
        if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalArgumentException("Unable inject a DBI Handle to a static field");
        }
        field.setAccessible(true);
        field.set(test, handle);
    }

    /**
     * Inject a DBI instance to a field with {@link DBIInstance} annotation.
     *
     * @param test  current test
     * @param field current field
     * @throws IllegalAccessException reflection error
     */
    private void handleDbiInstance(Object test, Field field) throws IllegalAccessException {
        if (!field.getType().equals(Jdbi.class)) {
            throw new IllegalArgumentException("Unable inject a DBI instance to " +
                    "a field with type " + field.getType());
        }
        if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalArgumentException("Unable inject a DBI instance to a static field");
        }
        field.setAccessible(true);
        field.set(test, dbi);
    }

    /**
     * Create and inject a new DBI SQL Object to a field
     * with {@link TestedSqlObject} annotation.
     *
     * @param test  current test
     * @param field current field
     * @throws IllegalAccessException reflection error
     */
    private void handleDbiSqlObject(Object test, Field field) throws IllegalAccessException {
        if (!(field.getType().isInterface() ||
                Modifier.isAbstract(field.getType().getModifiers()))) {
            throw new IllegalArgumentException("Unable inject a DBI SQL object to a field with type '"
                    + field.getType() + "'");
        }
        if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalArgumentException("Unable inject a DBI sql object to a static field");
        }
        field.setAccessible(true);
        field.set(test, handle.attach(field.getType()));
    }

    /**
     * Create a inject a DBI DAO instance to a field with the {@link TestedDao}  annotation.
     * <p>The DAO should provide a default constructor or a constructor
     * that accepts a {@link Jdbi} as the single parameter<p>
     *
     * @param test  current test
     * @param field current field
     * @throws IllegalAccessException reflection error
     */
    private void handleDbiDao(Object test, Field field) throws IllegalAccessException {
        if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalArgumentException("Unable inject a DBI DAO to a static field");
        }

        field.setAccessible(true);
        field.set(test, createDBIDao(field));
    }

    private Object createDBIDao(Field field) throws IllegalAccessException {
        // Find appropriate constructors
        Constructor<?> defaultConstructor = null;
        for (Constructor<?> constructor : field.getType().getDeclaredConstructors()) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (parameterTypes.length == 1 && parameterTypes[0].equals(Jdbi.class)) {
                // If a constructor with a DBI is provided, just invoke it
                try {
                    constructor.setAccessible(true);
                    return constructor.newInstance(dbi);
                } catch (Exception e) {
                    throw new RuntimeException("Unable to create an instance of class '"
                            + field.getDeclaringClass() + "'", e);
                }
            } else if (parameterTypes.length == 0) {
                defaultConstructor = constructor;
            }
        }

        if (defaultConstructor == null) {
            // No eligible constructor is provided
            throw new IllegalStateException("Unable find a constructor for class '"
                    + field.getDeclaringClass() + "'");
        }

        // A default constructor is provided.
        // Invoke it, find a DBI field and set a DBI context to it.
        Object dbiDao;
        defaultConstructor.setAccessible(true);
        try {
            dbiDao = defaultConstructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Unable to create an instance of class '"
                    + field.getDeclaringClass() + "'", e);
        }

        for (Field classField : field.getType().getDeclaredFields()) {
            if (classField.getType().equals(Jdbi.class)) {
                classField.setAccessible(true);
                classField.set(dbiDao, dbi);
                return dbiDao;
            }
        }

        throw new IllegalStateException("Unable find a field with type DBI");
    }
}