package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.config.ReplicationConfig;
import com.atlassian.bitbucket.mesh.transaction.Transaction;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ContentHashHelperTest {

    @Mock
    private ReplicationConfig config;
    @InjectMocks
    private ContentHashHelper contentHashHelper;
    @Mock
    private TransactionManager transactionManager;

    @Test
    public void testRandomnessFrequencyWithRealTransactionIDs() throws IOException {
        long maxLines = 2000L; //Change this to a higher value for a more thorough test but longer test runtime
        double targetFrequency = 0.01d;
        double permittedVariance = 0.1d;

        List<String> transactionIDs = getRealTransactionIds(maxLines);

        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(targetFrequency);

        int recalcuateCount = 0;
        for (String transactionID : transactionIDs) {
            Transaction transaction = transactionWithId(transactionID);
            when(transactionManager.getCurrentTransaction()).thenReturn(Optional.of(transaction));
            if (contentHashHelper.shouldForceRecalculate()) {
                recalcuateCount++;
            }
        }

        double frequency = recalcuateCount / (double) transactionIDs.size();

        assertThat("Recalculated " + recalcuateCount + " out of " + transactionIDs.size() + " transactions",
                Math.abs(frequency - targetFrequency), lessThan(targetFrequency * permittedVariance));
    }

    @Test
    public void testShouldForceRecalculateConfigAlways() {
        Transaction transaction = transactionWithId("ignored");
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(1d);
        when(transactionManager.getCurrentTransaction()).thenReturn(Optional.of(transaction));

        //Asserts that force recalculation is _always_ true when config is set to 100%; even when highest possible
        //random value is used
        assertTrue(contentHashHelper.shouldForceRecalculate());

        //The transaction ID is not manipulated if the config is 100%
        verifyNoInteractions(transaction);
    }

    @Test
    public void testShouldForceRecalculateConfigNever() {
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(0d);

        //Asserts that force recalculation is _always_ false when config is set to 0%; even when lowest possible random
        //value is used
        assertFalse(contentHashHelper.shouldForceRecalculate());

        //Transaction is not retrieved when config is set to 0%
        verifyNoInteractions(transactionManager);
    }

    @Test
    public void testShouldForceRecalculateNoTransaction() {
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(1d);
        when(transactionManager.getCurrentTransaction()).thenReturn(Optional.empty());

        assertFalse(contentHashHelper.shouldForceRecalculate());
    }

    @Test
    public void testShouldForceRecalculateRandomEqual() {
        //"n/22/tx/a7c686ca0000000000008028" results in a calculated random value of 0.100046.. (very close to but just
        //higher than the config for this test)
        Transaction transaction = transactionWithId("n/22/tx/a7c686ca0000000000008028");
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(0.1d);
        when(transactionManager.getCurrentTransaction()).thenReturn(Optional.of(transaction));

        //Asserts that force recalculation is false when random value is equal to config
        assertFalse(contentHashHelper.shouldForceRecalculate());
    }

    @Test
    public void testShouldForceRecalculateRandomHigher() {
        //"n/22/tx/a7c686ca0000000000007e14" results in a calculated random value of 0.500d when scaled to three decimal
        //places
        Transaction transaction = transactionWithId("n/22/tx/a7c686ca0000000000007e14");
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(0.45d);
        when(transactionManager.getCurrentTransaction()).thenReturn(Optional.of(transaction));

        //Asserts that force recalculation is false when random value is greater than config
        assertFalse(contentHashHelper.shouldForceRecalculate());
    }

    @Test
    public void testShouldForceRecalculateRandomLower() {
        //"n/22/tx/a7c686ca0000000000007e24" results in a calculated random value of 0.01d when scaled to three decimal
        //places
        Transaction transaction = transactionWithId("n/22/tx/a7c686ca0000000000007e24");
        when(config.getContentHashForcedRecalculationFrequency()).thenReturn(0.02d);
        when(transactionManager.getCurrentTransaction()).thenReturn(Optional.of(transaction));

        //Asserts that force recalculation is true when random value is less than config
        assertTrue(contentHashHelper.shouldForceRecalculate());
    }

    private static Transaction transactionWithId(String id) {
        Transaction transaction = mock(Transaction.class);
        when(transaction.getId()).thenReturn(id);
        return transaction;
    }

    private List<String> getRealTransactionIds(long maxLines) throws IOException {
        List<String> transactionIDs = new ArrayList<>();
        URL realTransactionIds =
                requireNonNull(getClass().getResource("real-transaction-ids.txt"), "real-transaction-ids.txt");
        try (InputStream in = realTransactionIds.openStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
            String line;
            long lines = 0L;
            while ((line = reader.readLine()) != null && lines < maxLines) {
                if (!line.isEmpty()) {
                    transactionIDs.add(line);
                    lines++;
                }
            }
        }
        return transactionIDs;
    }
}
