package com.atlassian.stash.internal.repository.sync.dao.v1;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.activeobjects.external.ModelVersion;
import com.atlassian.activeobjects.spi.DatabaseType;
import com.atlassian.activeobjects.test.TestActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDaoTest;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.stash.internal.repository.sync.dao.AoRejectedRef;
import com.atlassian.stash.internal.repository.sync.dao.v0.AoRejectedRefV0;
import com.google.common.collect.ImmutableMap;
import net.java.ao.EntityManager;
import net.java.ao.Query;
import net.java.ao.test.jdbc.Data;
import net.java.ao.test.jdbc.DatabaseUpdater;
import net.java.ao.test.jdbc.NonTransactional;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.atlassian.stash.internal.repository.sync.dao.v0.AoRejectedRefV0.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Data(MigrateToV1TaskTest.RejectedRefUpdater.class)
public class MigrateToV1TaskTest extends AbstractAoDaoTest {
    private static final String REJECTED_REF_TABLE_NAME = "REJECTED_REF";

    private static final int SHORT_ID = 449;
    private static final int EXACT_ID = 450;
    private static final int LONG_ID = 451;

    private static final String SHORT = StringUtils.rightPad("", 449, "s");
    private static final String EXACT = StringUtils.rightPad("", 450, "e");
    private static final String LONG = StringUtils.rightPad("", 451, "l");
    private static final String TRUNCATED = StringUtils.rightPad("", 450, "l");

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    private ActiveObjects ao;
    private ApplicationPropertiesService applicationPropertiesService;

    @Before
    public void setUp() throws Exception {
        ao = new TestActiveObjects(entityManager);
        applicationPropertiesService = mock(ApplicationPropertiesService.class);
    }

    @NonTransactional
    @Test
    public void testShortValueIsNotTruncated() throws IOException {
        performUpgradeAndGetDumpDirectory();

        AoRejectedRef[] rejectedRefs = ao.find(AoRejectedRef.class, Query.select().where(REPOSITORY_ID_COLUMN + " = " + SHORT_ID));

        assertThat(rejectedRefs, is(arrayWithSize(1)));
        AoRejectedRef rejectedRef = rejectedRefs[0];

        assertThat(rejectedRef.getRefDisplayId(), equalTo(SHORT));
        assertThat(rejectedRef.getRefId(), equalTo(SHORT));
    }

    @NonTransactional
    @Test
    public void testExactValueIsNotTruncated() throws IOException {
        performUpgradeAndGetDumpDirectory();

        AoRejectedRef[] rejectedRefs = ao.find(AoRejectedRef.class, Query.select().where(REPOSITORY_ID_COLUMN + " = " + EXACT_ID));

        assertThat(rejectedRefs, is(arrayWithSize(1)));
        AoRejectedRef rejectedRef = rejectedRefs[0];

        assertThat(rejectedRef.getRefDisplayId(), equalTo(EXACT));
        assertThat(rejectedRef.getRefId(), equalTo(EXACT));
    }

    @NonTransactional
    @Test
    public void testLongValueIsDumpedAndTruncated() throws IOException {
        File dataDirectory = performUpgradeAndGetDumpDirectory();

        AoRejectedRef[] rejectedRefs = ao.find(AoRejectedRef.class, Query.select().where(REPOSITORY_ID_COLUMN + " = " + LONG_ID));

        assertThat(rejectedRefs, is(arrayWithSize(1)));
        AoRejectedRef rejectedRef = rejectedRefs[0];

        assertThat(rejectedRef.getRefDisplayId(), equalTo(TRUNCATED));
        assertThat(rejectedRef.getRefId(), equalTo(TRUNCATED));

        File rejectedRefDumpFile = new File(dataDirectory,
                REJECTED_REF_TABLE_NAME + MigrateRejectedRefToV1Subtask.DUMP_FILE_SUFFIX);
        assertTrue(rejectedRefDumpFile.exists());

        List<String> lines = Files.lines(rejectedRefDumpFile.toPath()).collect(Collectors.toList());
        assertThat(lines.size(), equalTo(2)); // header and one row

        // check there are two columns (REF_DISPLAY_ID and REF_ID) containing large values
        assertThat(Pattern.compile(",")
                        .splitAsStream(lines.get(1))
                        .filter(LONG::equals)
                        .count(),
                equalTo(2L));
    }

    private File performUpgradeAndGetDumpDirectory() throws IOException {
        File temporaryFolder = folder.newFolder();
        when(applicationPropertiesService.getSharedHomeDir()).thenReturn(temporaryFolder);

        new MigrateToV1Task(applicationPropertiesService).upgrade(ModelVersion.valueOf("0"), ao);

        return temporaryFolder;
    }

    /**
     * The goal of this class is to create initial objects in DB, i.e. state before upgrade.
     * <p>
     * There's no way to create column of type VARCHAR(767) (767 is the old value of
     * {@link net.java.ao.schema.StringLength#MAX_LENGTH} which change was the reason for {@link MigrateToV1Task}
     * existence) by means of Active Objects because of following facts:
     * <ul>
     * <li>
     * if no {@link net.java.ao.schema.StringLength} restriction is set on a column, Active Objects create VARCHAR(255)
     * column (at least in H2 DB used in tests);
     * </li>
     * <li>
     * {@link net.java.ao.schema.StringLength} annotation's argument
     * {@link net.java.ao.schema.SchemaGenerator#getSQLTypeFromMethod(net.java.ao.types.TypeManager, Class,
     * java.lang.reflect.Method, net.java.ao.AnnotationDelegate) is obliged} to be not greater than 450;
     * </li>
     * <li>
     * {@link net.java.ao.schema.StringLength#UNLIMITED} would convert column to CLOB.
     * </li>
     * </ul>
     * <p>
     * Therefore this class creates table via raw SQL query.
     */
    public static class RejectedRefUpdater implements DatabaseUpdater {
        @Override
        public void update(EntityManager entityManager) throws Exception {
            assumeTrue("The updater for this migration test only runs on H2 at present", isDatabase(DatabaseType.H2));

            Statement statement = entityManager.getProvider().getConnection().createStatement();
            statement.execute(String.format(
                    "create table AO_000000_%s (" +
                            "%s INT PRIMARY KEY AUTO_INCREMENT, " +
                            "%s VARCHAR(767)," +
                            "%s VARCHAR(767), " +
                            "%s INT, " +
                            "%s INT)",
                    REJECTED_REF_TABLE_NAME,
                    "ID",
                    REF_DISPLAY_ID_COLUMN,
                    REF_ID_COLUMN,
                    REPOSITORY_ID_COLUMN,
                    STATE_COLUMN));

            createRejectedRef(entityManager, SHORT_ID, SHORT, SHORT);
            createRejectedRef(entityManager, EXACT_ID, EXACT, EXACT);
            createRejectedRef(entityManager, LONG_ID, LONG, LONG);
        }

        private AoRejectedRefV0 createRejectedRef(EntityManager entityManager,
                                                  int repositoryId,
                                                  String refDisplayId,
                                                  String refId) throws SQLException {
            return entityManager.create(AoRejectedRefV0.class,
                    ImmutableMap.of(
                            REF_DISPLAY_ID_COLUMN, refDisplayId,
                            REF_ID_COLUMN, refId,
                            REPOSITORY_ID_COLUMN, repositoryId,
                            STATE_COLUMN, 0));
        }
    }
}