package com.atlassian.bitbucket.ao;

import com.atlassian.activeobjects.spi.DatabaseType;
import com.google.common.collect.ImmutableMap;
import net.java.ao.Accessor;
import net.java.ao.EntityManager;
import net.java.ao.RawEntity;
import net.java.ao.schema.Indexed;
import net.java.ao.schema.PrimaryKey;
import net.java.ao.schema.Unique;
import net.java.ao.test.jdbc.Jdbc;
import net.java.ao.test.junit.ActiveObjectsJUnitRunner;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableList;
import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

@RunWith(ActiveObjectsJUnitRunner.class)
@Jdbc(SystemPropertyJdbcConfiguration.class)
@Category(AbstractAoDaoTest.class)
public abstract class AbstractAoDaoTest {

    private static final Pattern JDBC_PATTERN = Pattern.compile("^(jdbc(?::\\w+)+)(?:\\W.*)?");

    protected EntityManager entityManager;

    private final List<Class<? extends RawEntity<?>>> entityClasses;

    public AbstractAoDaoTest() {
        entityClasses = null;
    }

    @SafeVarargs
    public AbstractAoDaoTest(Class<? extends RawEntity<?>> entityClass,
                             Class<? extends RawEntity<?>>... entityClasses) {
        this.entityClasses = Stream.concat(Stream.of(entityClass), Stream.of(entityClasses))
                .filter(Objects::nonNull)
                .collect(toImmutableList());
    }

    @Test
    public void testAnnotations() {
        if (entityClasses == null || entityClasses.isEmpty()) {
            return;
        }

        ImmutableMap.Builder<String, List<String>> builder = ImmutableMap.builder();
        for (Class<? extends RawEntity<?>> entityClass : entityClasses) {
            List<String> invalidAnnotations = checkAnnotations(entityClass);
            if (!invalidAnnotations.isEmpty()) {
                builder.put(entityClass.getSimpleName(), invalidAnnotations);
            }
        }

        Map<String, List<String>> invalidEntities = builder.build();
        if (!invalidEntities.isEmpty()) {
            fail("One or more entities have invalid annotations:\n\n" +
                    invalidEntities.entrySet().stream()
                            .map(entry -> entry.getKey() + toBulletedList(entry.getValue()))
                            .collect(Collectors.joining("\n")));
        }
    }

    @Test
    public void testUsingCorrectDatabase() throws Exception {
        Connection connection = null;
        try {
            connection = entityManager.getProvider().getConnection();
            String expectedUrl = getJdbcUrl();
            assertEquals(getJdbcProtocolFor(expectedUrl), getJdbcProtocolFor(connection.getMetaData().getURL()));
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException ignored) {
                    // no-op
                }
            }
        }
    }

    protected static boolean isDatabase(DatabaseType dbType) {
        String protocol = getJdbcProtocol();
        if (protocol == null) {
            return false;
        }

        switch (dbType) {
            case H2:
                return protocol.startsWith("jdbc:h2");
            case HSQL:
                return protocol.startsWith("jdbc:hsqldb");
            case MYSQL:
                return protocol.startsWith("jdbc:mysql");
            case MS_SQL:
                return protocol.startsWith("jdbc:sqlserver");
            case ORACLE:
                return protocol.startsWith("jdbc:oracle:thin");
            case POSTGRESQL:
                return protocol.startsWith("jdbc:postgresql");
            default:
                throw new IllegalArgumentException("Unable to identify databases of type " + dbType);
        }
    }

    private static List<String> checkAnnotations(Class<? extends RawEntity<?>> entityClass) {
        requireNonNull(entityClass, "entityClass");

        List<String> invalidAnnotations = new ArrayList<>();
        for (Method method : entityClass.getMethods()) {
            PrimaryKey primaryKey = method.getAnnotation(PrimaryKey.class);

            boolean indexed = method.isAnnotationPresent(Indexed.class);
            boolean unique = method.isAnnotationPresent(Unique.class);

            if (primaryKey != null) {
                String name = primaryKey.value().trim().isEmpty() ?
                        method.getName() :
                        "\"" + primaryKey.value() + "\" (" + method.getName() + ")";

                if (indexed) {
                    invalidAnnotations.add("@PrimaryKey " + name + " is inherently @Indexed");
                }
                if (unique) {
                    invalidAnnotations.add("@PrimaryKey " + name + " is inherently @Unique");
                }
            } else if (indexed && unique) {
                Accessor accessor = method.getAnnotation(Accessor.class);
                String name = accessor == null ?
                        method.getName() :
                        "\"" + accessor.value() + "\" (" + method.getName() + ")";

                invalidAnnotations.add("@Unique column " + name + " is inherently @Indexed");
            }
        }

        return invalidAnnotations;
    }

    private static String getJdbcProtocol() {
        return getJdbcProtocolFor(getJdbcUrl());
    }

    private static String getJdbcProtocolFor(String url) {
        // Normalise SQL Server URLs
        if (url.startsWith(SystemPropertyJdbcConfiguration.PROTOCOL_JTDS)) {
            return SystemPropertyJdbcConfiguration.PROTOCOL_SQLSERVER;
        }

        Matcher matcher = JDBC_PATTERN.matcher(url);
        return matcher.find() ? matcher.group(1) : null;
    }

    private static String getJdbcUrl() {
        return System.getProperty(SystemPropertyJdbcConfiguration.PROP_JDBC_URL, "jdbc:h2:mem:ao");
    }

    private static String toBulletedList(List<String> messages) {
        return messages.stream()
                .collect(Collectors.joining("\n- ", "\n- ", ""));
    }
}
