package com.atlassian.stash.internal.liquibase;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.ProxyUtils;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.repository.*;
import liquibase.changelog.ChangeLogIterator;
import liquibase.changelog.ChangeLogParameters;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.changelog.filter.DbmsChangeSetFilter;
import liquibase.changelog.visitor.UpdateVisitor;
import liquibase.database.Database;
import liquibase.exception.LiquibaseException;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import org.junit.Test;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Verifies that the two changesets in {@code liquibase/r4_0/p07.xml} correctly update bundled hook keys from
 * Stash to Bitbucket Server.
 * <p>
 * Note that there is no custom change associated with this test; it's testing standard Liquibase changesets
 * to ensure the SQL they produce executes correctly on all supported databases and updates the correct rows.
 */
@DataSet({DataSets.PROJECTS, DataSets.REPOSITORIES,
        DataSets.PREFIX + "liquibase/update_bundled_hook_keys" + DataSets.SUFFIX})
public class UpdateBundledHookKeysChangeTest extends AbstractDaoTest {

    private static final String CHANGELOG_FILE = "liquibase/r4_0/p07.xml";
    private static final String LEGACY_HOOK_KEY = "com.atlassian.stash.stash-bundled-hooks:force-push-hook";
    private static final String HOOK_KEY = "com.atlassian.bitbucket.server.bitbucket-bundled-hooks:force-push-hook";

    @Dao(HibernateRepositoryHookDao.class)
    private RepositoryHookDao hookDao;
    @Dao(HibernateRepositoryDao.class)
    private RepositoryDao repositoryDao;

    @Test
    public void testHookUpdate() {
        InternalRepository legacy = repositoryDao.loadById(1);
        assertNotNull("Repository 1 should have a legacy hook", hookDao.findByKey(legacy, LEGACY_HOOK_KEY));
        assertNull("Repository 1 should not have an updated hook", hookDao.findByKey(legacy, HOOK_KEY));

        InternalRepository updated = repositoryDao.loadById(2);
        assertNull("Repository 2 should not have a legacy hook", hookDao.findByKey(updated, LEGACY_HOOK_KEY));
        assertNotNull("Repository 2 should have an updated hook", hookDao.findByKey(updated, HOOK_KEY));

        InternalRepository both = repositoryDao.loadById(3);
        assertNotNull("Repository 3 should have a legacy hook", hookDao.findByKey(both, LEGACY_HOOK_KEY));
        assertNotNull("Repository 3 should have an updated hook", hookDao.findByKey(both, HOOK_KEY));

        runChangesets();

        //In order to be sure we get the right data below, we need to clear the caches. Otherwise we might
        //still see the legacy key for repository 1 even though it's actually been updated in the database
        clear();

        assertNull("Repository 1 should no longer have a legacy hook", hookDao.findByKey(legacy, LEGACY_HOOK_KEY));
        assertNull("Repository 2 should still not have a legacy hook", hookDao.findByKey(updated, LEGACY_HOOK_KEY));
        assertNull("Repository 3 should no longer have a legacy hook", hookDao.findByKey(both, LEGACY_HOOK_KEY));

        assertNotNull("Repository 1 should now have an updated hook", hookDao.findByKey(legacy, HOOK_KEY));
        assertNotNull("Repository 2 should still have an updated hook", hookDao.findByKey(updated, HOOK_KEY));
        assertNotNull("Repository 3 should still have an updated hook", hookDao.findByKey(both, HOOK_KEY));

        Page<InternalRepositoryHook> page = hookDao.findAll(PageUtils.newRequest(0, 5));
        assertNotNull(page);
        assertEquals(3, page.getSize());
        assertTrue(page.getIsLastPage());

        AtomicLong id = new AtomicLong(1L);
        page.stream().forEach(hook -> {
            assertEquals(id.getAndIncrement(), hook.getId());
            assertEquals("Hook " + hook.getId() + " has an incorrect key: " + hook.getHookKey(),
                    HOOK_KEY, hook.getHookKey());
        });
    }

    private static Connection wrapConnection(Connection connection) {
        return ProxyUtils.createProxy(Connection.class,
                (proxy, method, args) -> {
                    if ("commit".equals(method.getName()) ||
                            "rollback".equals(method.getName()) ||
                            "setAutoCommit".equals(method.getName())) {
                        //Ignore any attempts to commit or rollback or adjust the auto-commit setting
                        return null;
                    }

                    try {
                        return method.invoke(connection, args);
                    } catch (InvocationTargetException e) {
                        throw e.getCause();
                    }
                });
    }

    /**
     * By the time this test runs, the changesets we want to test have already executed. To get Liquibase to
     * run them again, we need to do a little dance.
     * <p>
     * The approach taken here is to have Liquibase parse <i>only</i> the {@code p07.xml} changelog which,
     * since it's purely DML, stands on its own, and run it again.
     */
    private void runChangesets() {
        sessionFactory.getCurrentSession().doWork(connection -> {
            connection = wrapConnection(connection); //Wrap the connection to keep Liquibase from committing

            ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
            Database database = LiquibaseUtils.findDatabaseForConnection(connection);
            try {
                DatabaseChangeLog changeLog = ChangeLogParserFactory.getInstance()
                        .getParser(CHANGELOG_FILE, resourceAccessor)
                        .parse(CHANGELOG_FILE, new ChangeLogParameters(database), resourceAccessor);

                //Note that by not passing a ShouldRunChangeSetFilter here we ensure Liquibase runs the
                //changesets again even though they've already run
                new ChangeLogIterator(changeLog, new DbmsChangeSetFilter(database))
                        .run(new UpdateVisitor(database), database);
            } catch (LiquibaseException e) {
                throw new AssertionError(CHANGELOG_FILE + " could not be parsed.", e);
            }
        });
    }
}
