package com.atlassian.bitbucket.mesh.migration;

import com.atlassian.bitbucket.mesh.git.hook.GitHookOutputWriter;
import com.atlassian.bitbucket.mesh.git.ref.RefUpdate;
import com.atlassian.bitbucket.mesh.repository.BitbucketServerRepository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcProposedRefUpdate;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcTransaction;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcUpdateRefsRequest;
import com.atlassian.bitbucket.mesh.transaction.ReferenceTransaction;
import com.atlassian.bitbucket.mesh.transaction.TransactionLog;
import com.atlassian.bitbucket.mesh.util.Person;
import com.google.protobuf.ByteString;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

import static com.atlassian.bitbucket.mesh.transaction.ReferenceTransactionState.COMMITTED;
import static com.atlassian.bitbucket.mesh.transaction.ReferenceTransactionState.PREPARED;
import static com.atlassian.bitbucket.mesh.transaction.RpcTransactionMatcher.tx;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class SidecarMigrationTransactionListenerTest {

    private static final RefUpdate BRANCH_ADD = RefUpdate.ofAdd(
            "refs/heads/branch", "1234567890123456789012345678901234567890");
    private static final String FQ_TARGET_REPOSITORY_ID = "p/0011/h/abcde1234567890/r/2321";
    private static final String HIERARCHY_ID = "abcde1234567890";
    private static final int TARGET_PARTITION = 17;
    private static final Person USER = new Person("username", "user@domain.com");

    private SidecarMigrationTransactionListener listener;
    @Mock
    private HierarchyMigrationState migrationState;
    @Mock
    private MigrationStateManager migrationStateManager;
    @Mock
    private BitbucketServerRepository repository;
    @Captor
    private ArgumentCaptor<RpcTransaction> transactionCaptor;
    @Mock
    private TransactionLog transactionLog;
    @Mock
    private GitHookOutputWriter writer;

    @Before
    public void setUp() {
        when(repository.getBitbucketId()).thenReturn("2321");
        when(repository.getHierarchyId()).thenReturn(HIERARCHY_ID);
        when(migrationStateManager.getForHierarchy(anyString())).thenReturn(empty());
        when(migrationState.getTargetPartition()).thenReturn(TARGET_PARTITION);
        when(migrationState.getTransactionLog()).thenReturn(transactionLog);
        when(migrationState.isRunning()).thenReturn(true);

        listener = new SidecarMigrationTransactionListener(migrationStateManager);
    }

    @Test
    public void testCommittedPullRequestRefReferenceTransactionIsIgnoredWhenTracking() {
        enableTracking();

        listener.onReferenceTransaction(new ReferenceTransaction.Builder(repository, COMMITTED, writer)
                .refUpdates(RefUpdate.of("stash-refs/pull-requests/1234/from",
                        "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", "1111111111222222222233333333334444444444"))
                .user(USER)
                .build());

        verifyNoInteractions(transactionLog);
    }

    @Test
    public void testCommittedReferenceTransactionIsIgnoredWhenNotTracking() {
        listener.onReferenceTransaction(new ReferenceTransaction.Builder(repository, COMMITTED, writer)
                .refUpdates(BRANCH_ADD)
                .user(USER)
                .build());

        verifyNoInteractions(transactionLog);
    }

    @Test
    public void testCommittedReferenceTransactionIsRecordedWhenTracking() {
        enableTracking();

        listener.onReferenceTransaction(new ReferenceTransaction.Builder(repository, COMMITTED, writer)
                .refUpdates(BRANCH_ADD)
                .user(USER)
                .build());

        assertTransactions(RpcTransaction.newBuilder()
                .setPerson(USER.toPerson())
                .setRepository(FQ_TARGET_REPOSITORY_ID)
                .setUpdateRefs(RpcUpdateRefsRequest.newBuilder()
                        .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                .setRefId(ByteString.copyFromUtf8(BRANCH_ADD.getRefId()))
                                .setNewHash(BRANCH_ADD.getNewHash())) //old hash should _not_ be recorded
                        .setRepository(FQ_TARGET_REPOSITORY_ID)
                        .setTransactional(true))
                .build());
    }

    @Test
    public void testPreparedReferenceTransactionIsIgnoredWhenTracking() {
        enableTracking();

        ReferenceTransaction referenceTransaction = new ReferenceTransaction.Builder(repository, PREPARED, writer)
                .refUpdates(BRANCH_ADD)
                .user(USER)
                .build();
        listener.onReferenceTransaction(referenceTransaction);

        verifyNoInteractions(transactionLog);
        assertFalse(referenceTransaction.isRejected());
    }

    @Test
    public void testPreparedReferenceTransactionIsRejectedWhenReadOnly() {
        enableTracking();
        when(migrationState.isCompleted()).thenReturn(true);

        ReferenceTransaction referenceTransaction = new ReferenceTransaction.Builder(repository, PREPARED, writer)
                .refUpdates(BRANCH_ADD)
                .user(USER)
                .build();
        listener.onReferenceTransaction(referenceTransaction);

        verifyNoInteractions(transactionLog);
        assertTrue(referenceTransaction.isRejected());
    }

    private void assertTransactions(RpcTransaction... transactions) {
        verify(transactionLog, times(transactions.length)).append(transactionCaptor.capture());
        List<RpcTransaction> actualTransactions = transactionCaptor.getAllValues();

        for (int i = 0; i < transactions.length; ++i) {
            assertThat(actualTransactions.get(i), tx(transactions[i]));
        }
    }

    private void enableTracking() {
        when(migrationStateManager.getForHierarchy(anyString())).thenReturn(of(migrationState));
    }
}