package com.sonymusic

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.FieldSource

import static org.junit.jupiter.api.Assertions.*

class WithBackoffTest extends BaseGlobalVarTest {

    def withBackoff

    @BeforeEach
    void setUp() {
        super.setUp()
        withBackoff = loadScript("vars/withBackoff.groovy")
        helper.registerAllowedMethod('sleep', [Map])
    }

    @ParameterizedTest(name='{1}')
    @FieldSource
    void testInvalidParameters(Map args, String expectedMessage) {
        def beenCalled = false
        assertThrowsWithMessage(
            AssertionError.class,
            expectedMessage,
            {
                withBackoff.call(args) {
                    beenCalled = true
                }
            }
        )
        assertFalse(beenCalled, 'Expected closure to not been called')
    }

    private static List<Arguments> testInvalidParameters = Arrays.asList(
        // args, expectedMessage
        Arguments.of([strategy: 'unknown'], 'Invalid backoff strategy'),
        Arguments.of([attempts: 0], 'attempts must be ≥ 1'),
        Arguments.of([delay: -1], 'delay must be ≥ 0'),
        Arguments.of([maxDelay: -5], 'maxDelay must be ≥ 0'),
        Arguments.of([factor: 0], 'factor must be ≥ 1'),
    );

    @ParameterizedTest(name='{argumentSetName}')
    @FieldSource
    void testBackoffStrategies(String strategy, Integer attempts, Integer delay, Integer factor, Integer maxDelay, Integer expectedFinalDelay) {
        def attemptsMade = 0

        def args = [
            attempts: attempts,
            delay   : delay,
            strategy: strategy,
        ]

        if (factor != null) args.factor = factor
        if (maxDelay != null) args.maxDelay = maxDelay

        assertThrows(FailedOnPurposeException.class, {
            withBackoff.call(args) {
                attemptsMade++
                throw new FailedOnPurposeException()
            }
        })

        assertEquals(attempts, attemptsMade)
        assertMethodCalledTimes('sleep', attempts - 1)
        assertMethodCalledNthWith('sleep', attempts - 2, [time: expectedFinalDelay, unit: 'SECONDS'])
    }

    private static List<Arguments> testBackoffStrategies = Arrays.asList(
        // set name, strategy, attempts, delay, factor, maxDelay, expectedFinalDelay
        Arguments.argumentSet('constant [2]', 'constant', 2, 2, null, null, 2),
        Arguments.argumentSet('constant [2 → capped to 1]', 'constant', 3, 2, null, 1, 1),
        Arguments.argumentSet('linear [2 * (1) = 2]', 'linear', 2, 2, null, null, 2),
        Arguments.argumentSet('linear [2 * (2) = 4 → capped to 3]', 'linear', 3, 2, null, 3, 3),
        Arguments.argumentSet('exponential [2 * (2^1) = 4]', 'exponential', 3, 2, 2, null, 4),
        Arguments.argumentSet('exponential [2 * (3^2) = 18 → capped to 10]', 'exponential', 4, 2, 3, 10, 10),
    );

    @DisplayName('No backoff on success')
    @Test
    void testNoBackoffOnSuccess() {
        def attemptsMade = 0
        withBackoff {
            attemptsMade++
        }
        assertEquals(attemptsMade, 1)
        assertMethodCalledTimes('sleep', 0)
    }

    // Support class
    static class FailedOnPurposeException extends RuntimeException {}
}
