package com.atlassian.bitbucket.mesh.config;

import org.junit.Test;

import java.time.Duration;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class ExponentialBackoffTest {

    @Test
    public void testExponentialDelay() {
        ExponentialBackoff backoff = new ExponentialBackoff(Duration.ofSeconds(1L), Duration.ofSeconds(10L), 2.0, 0.0);

        //Loop 3 times to verify that executions are independent
        for (int i = 0; i < 3; ++i) {
            Backoff.Execution execution = backoff.start();

            assertNotNull(execution);
            assertEquals(Duration.ofSeconds(1L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(2L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(4L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(8L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(10L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(10L), execution.nextDelay());
            assertEquals(Duration.ofSeconds(10L), execution.nextDelay());
        }
    }

    @Test
    public void testJitter() {
        double multiplier = 1.15;
        double jitter = 0.1;
        ExponentialBackoff backoff =
                new ExponentialBackoff(Duration.ofSeconds(1L), Duration.ofSeconds(1000L), multiplier, jitter);

        Backoff.Execution execution = backoff.start();
        assertNotNull(execution);

        long prevMillis = execution.nextDelay().toMillis();
        long maxMillis = 1_000_000L;
        int negativeJitterCount = 0;
        int positiveJitterCount = 0;
        int zeroJitterCount = 0;
        //Enough iterations to hit max delay, regardless of how the jitter comes out
        for (int i = 1; i < 100; ++i) {
            long nextMillis = execution.nextDelay().toMillis();
            long expectedNoJitter = Math.min((long) (prevMillis * multiplier), maxMillis);
            assertThat("Delay " + i + " was not within expected jitter", nextMillis, both(
                    greaterThanOrEqualTo((long) (expectedNoJitter * (1 - jitter)))).and(
                    lessThanOrEqualTo(Math.min(maxMillis, (long) Math.ceil(expectedNoJitter * (1 + jitter))))));
            long actualJitter = nextMillis - expectedNoJitter;
            if (actualJitter == 0L) {
                zeroJitterCount++;
            } else if (actualJitter < 0L) {
                negativeJitterCount++;
            } else {
                positiveJitterCount++;
            }
            //System.out.printf("%d: jitter = %.2f%%%n", i, 100.0 * actualJitter / expectedNoJitter);
            prevMillis = nextMillis;
        }
        assertThat("Jitter was not applied", zeroJitterCount, lessThan(100));
        assertThat("Jitter was never negative", negativeJitterCount, greaterThan(0));
        assertThat("Jitter was never positive", positiveJitterCount, greaterThan(0));
    }

    @Test
    public void testNegativeInitialDelay() {
        ExponentialBackoff backoff = new ExponentialBackoff(Duration.ofSeconds(-1L), Duration.ofSeconds(1L), 2.0, 0.0);

        assertEquals(Duration.ZERO, backoff.start().nextDelay());
    }

    @Test
    public void testNegativeMaxDelay() {
        ExponentialBackoff backoff = new ExponentialBackoff(Duration.ofSeconds(1L), Duration.ofSeconds(-1L), 2.0, 0.0);

        assertEquals(Duration.ZERO, backoff.start().nextDelay());
    }

    @Test
    public void testNegativeMultiplier() {
        ExponentialBackoff backoff = new ExponentialBackoff(Duration.ofSeconds(1L), Duration.ofSeconds(1L), -1.0, 0.0);

        Backoff.Execution execution = backoff.start();

        assertNotNull(execution);
        assertEquals(Duration.ofSeconds(1L), execution.nextDelay());
        assertEquals(Duration.ZERO, execution.nextDelay());
        assertEquals(Duration.ZERO, execution.nextDelay());
    }
}