package com.atlassian.stash.internal.backup.liquibase;

import liquibase.change.core.DeleteDataChange;
import liquibase.change.core.InsertDataChange;
import liquibase.changelog.ChangeSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import org.xml.sax.SAXException;

import java.util.Iterator;

import static com.atlassian.bitbucket.hamcrest.LiquibaseMatchers.columnConfig;
import static com.atlassian.bitbucket.hamcrest.LiquibaseMatchers.hasTableName;
import static com.atlassian.stash.internal.backup.liquibase.LiquibaseChangeSetTestUtils.*;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

/**
 * Tests the operation of the {@link DatabaseUpdater} class;
 */
@RunWith(MockitoJUnitRunner.class)
public class DatabaseUpdaterTest {

    private static final int COUNT_NON_EMPTY_CHANGES = 42;

    @Mock
    private LiquibaseDao dao;
    @Mock
    private Iterator<LiquibaseChangeSetMetaData> outline;
    @Mock
    private ChangeLogOutline changeLogOutline;
    @Mock
    private LiquibaseRestoreMonitor monitor;

    @Captor
    private ArgumentCaptor<InsertDataChange> insertCaptor;

    @Captor
    private ArgumentCaptor<DeleteDataChange> deleteCaptor;

    /** An instance of the class under test */
    private DatabaseUpdater updater;

    @Before
    public void setUp() {
        when(changeLogOutline.nonEmptyChangeSetCount()).thenReturn(COUNT_NON_EMPTY_CHANGES);
        when(changeLogOutline.iterator()).thenReturn(outline);

        updater = new DatabaseUpdater(dao, monitor, changeLogOutline);
    }

    /**
     * Checks that an insert operation is not performed when there are no columns.
     */
    @Test
    public void testNoColumnsNoInsert() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(1, 0);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "test", "test", "hsql");
        updater.onChangesetBegin(changeSet);
        updater.onChangesetComplete(changeSet);

        InOrder inOrder = inOrder(dao);
        inOrder.verify(dao).beginChangeSet();
        inOrder.verify(dao, never()).insert(any(InsertDataChange.class));
        inOrder.verify(dao).endChangeSet();
        inOrder.verifyNoMoreInteractions();
    }

    /**
     * Tests that an insert operation will go ahead despite the fact that it's containing changelog has
     * zero weight.
     */
    @Test
    public void testZeroWeight() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(1, 0);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "author", "test", "hsql");
        InsertDataChange insert = newInsertChange("MyTable", newColumnConfig("age", 32L));
        updater.onChangesetBegin(changeSet);
        updater.onChangesetContent(insert);
        updater.onChangesetComplete(changeSet);

        InOrder inOrder = inOrder(dao);
        inOrder.verify(dao).beginChangeSet();
        inOrder.verify(dao).insert(insertCaptor.capture());
        inOrder.verify(dao).endChangeSet();
        inOrder.verifyNoMoreInteractions();

        InsertDataChange change = insertCaptor.getValue();
        assertThat(change, hasTableName("MyTable"));
        assertThat(change.getColumns(), hasSize(1));
        assertThat(change.getColumns(), hasItem(columnConfig("age", 32L)));

        inOrder = inOrder(monitor);
        inOrder.verify(monitor).onBeginChangeset(eq(summary), eq(1), eq(COUNT_NON_EMPTY_CHANGES));
        inOrder.verify(monitor).onAppliedChange();
        inOrder.verify(monitor).onFinishedChangeset();
    }

    /**
     * Tests that advancing progress is reported when processing an insert.
     */
    @Test
    public void testInsertAdvancesProgress() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(1, 100);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "author", "test", "hsql");
        InsertDataChange insert = newInsertChange("MyTable", newColumnConfig("age", 32L));
        updater.onChangesetBegin(changeSet);
        updater.onChangesetContent(insert);
        updater.onChangesetComplete(changeSet);

        InOrder inOrder = inOrder(dao);
        inOrder.verify(dao).beginChangeSet();
        inOrder.verify(dao).insert(insertCaptor.capture());
        inOrder.verify(dao).endChangeSet();
        inOrder.verifyNoMoreInteractions();

        InsertDataChange change = insertCaptor.getValue();
        assertThat(change, hasTableName("MyTable"));
        assertThat(change.getColumns(), hasSize(1));
        assertThat(change.getColumns(), hasItem(columnConfig("age", 32L)));

        inOrder = inOrder(monitor);
        inOrder.verify(monitor).onBeginChangeset(eq(summary), eq(1), eq(COUNT_NON_EMPTY_CHANGES));
        inOrder.verify(monitor).onAppliedChange();
        inOrder.verify(monitor).onFinishedChangeset();
    }

    /**
     * Tests that advancing progress is reported when processing an insert.
     */
    @Test
    public void testDeleteAdvancesProgress() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(1, 100);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "author", "test", "hsql");
        DeleteDataChange delete = newDeleteChange("MyTable");
        updater.onChangesetBegin(changeSet);
        updater.onChangesetContent(delete);
        updater.onChangesetComplete(changeSet);

        InOrder inOrder = inOrder(dao);
        inOrder.verify(dao).beginChangeSet();
        inOrder.verify(dao).deleteAllRows(eq("MyTable"));
        inOrder.verify(dao).endChangeSet();
        inOrder.verifyNoMoreInteractions();

        inOrder = inOrder(monitor);
        inOrder.verify(monitor).onBeginChangeset(eq(summary), eq(1), eq(COUNT_NON_EMPTY_CHANGES));
        inOrder.verify(monitor).onAppliedChange();
    }

    /**
     * Tests that column values are decoded before insertion into the database
     */
    @Test
    public void testDecodeColumnValue() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(1, 100);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "author", "test", "hsql");
        InsertDataChange insert = newInsertChange("MyTable", newColumnConfig("verticaltab", "blah\u000Cblah"));
        updater.onChangesetBegin(changeSet);
        updater.onChangesetContent(insert);
        updater.onChangesetComplete(changeSet);

        InOrder inOrder = inOrder(dao);
        inOrder.verify(dao).beginChangeSet();
        inOrder.verify(dao).insert(insertCaptor.capture());
        inOrder.verify(dao).endChangeSet();
        inOrder.verifyNoMoreInteractions();

        InsertDataChange change = insertCaptor.getValue();
        assertThat(change, hasTableName("MyTable"));
        assertThat(change.getColumns(), hasSize(1));
        assertThat(change.getColumns(), hasItem(columnConfig("verticaltab", "blah\u000Cblah")));
    }

    /**
     * Tests that an empty changeset is not applied.
     */
    @Test
    public void testEmptyChangeset() throws SAXException {
        LiquibaseChangeSetMetaData summary = new LiquibaseChangeSetMetaData(0, 100);
        when(outline.hasNext()).thenReturn(true);
        when(outline.next()).thenReturn(summary);

        ChangeSet changeSet = newChangeSet("1", "author", "test", "hsql");
        updater.onChangesetBegin(changeSet);
        updater.onChangesetComplete(changeSet);

        verifyZeroInteractions(dao);
    }

}
