package com.atlassian.bitbucket.mesh.throttle;

import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.platform.OperatingSystemService;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ObjectArrays;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.mock.env.MockEnvironment;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.concurrent.TimeUnit;

import static com.atlassian.bitbucket.mesh.throttle.ThrottledResource.COMMAND;
import static com.atlassian.bitbucket.mesh.throttle.ThrottledResource.PACK;
import static com.atlassian.bitbucket.mesh.throttle.ThrottlingConstants.*;
import static com.google.common.primitives.Ints.saturatedCast;
import static java.lang.Math.round;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.Silent.class)
public class ResourceThrottleStrategyProviderTest {

    private static final long MEM_1MB = 1024 * 1024;
    private static final long MEM_1GB = 1024 * MEM_1MB;
    private static final long DEFAULT_WEBAPP_MAX_MEM = 512 * MEM_1MB;

    @Rule
    public LogListener logListener = new LogListener(ResourceThrottleStrategyProvider.class); // suppress logging

    @Mock
    private Clock clock;
    @Mock
    private OperatingSystemService operatingSystemService;

    @Before
    public void setUp() {
        when(operatingSystemService.getCpuLoad()).thenReturn(OptionalDouble.of(0.5d));
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(Long.MAX_VALUE));
        when(clock.millis()).thenReturn(System.currentTimeMillis());
    }

    @Test
    public void testInvalidStrategyRevertsToFixed() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=foobar",
                "throttle.resource.scm-hosting.timeout=501");

        doTestFallbackToFixedStrategy(provider, PACK, fallbackFixedTicketsFor(PACK), 501);

        logListener.assertWarn("Invalid throttling strategy");
    }

    @Test
    public void testAdaptiveStrategyForNonHostingTicketsRevertsToFixed() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command.strategy=adaptive",
                "throttle.resource.scm-command.timeout=500");

        // scm-command's default fixed ticket limit is 25
        doTestFallbackToFixedStrategy(provider, COMMAND, fallbackFixedTicketsFor(COMMAND), 500);

        logListener.assertWarn("Adaptive throttling is not compatible with this resource");
    }

    @Test
    public void testAdaptiveStrategy() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.fixed.limit=20", // used when CPU is not available for an extended period
                "throttle.resource.scm-hosting.timeout=500");

        doTestAdaptiveStrategy(provider, PACK, 15, 50, 20, 20, 500, 10, 0.82d, 0.03d, 0.25d);
    }

    @Test
    public void testAdaptiveStrategyReducesMaxTicketsIfMemoryConstrained() {
        // 8GB total memory
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(8L * MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                // 1GB JVM max memory (-Xmx)
                MEM_1GB,
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                "throttle.resource.scm-hosting.adaptive.mem.reserved=1024",
                "throttle.resource.scm-hosting.timeout=500");

        // (8GB-1GB-1GB)/256MB = max 24 hosting ops
        doTestAdaptiveStrategy(provider, PACK, 15, 24, fallbackFixedTicketsFor(PACK),
                fallbackFixedTicketsFor(PACK), 500, 10, 0.82d, 0.03d, 0.25d);
    }

    @Test
    public void testAdaptiveStrategyRevertsToFixedIfMemoryConstrainedAndMinNotSatisfied() {
        // 1GB total memory
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                // 256MB JVM max memory (-Xmx)
                256 * MEM_1MB,
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.fixed.limit=10",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                "throttle.resource.scm-hosting.adaptive.mem.reserved=256",
                "throttle.resource.scm-hosting.adaptive.mem.webapp=jvmmax",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 10, 500);

        logListener.assertWarn(
                "This machine's total memory cannot safely support the configured minimum number of tickets");
    }

    @Test
    public void testAdaptiveStrategyRevertsToFixedIfTotalSystemMemoryNotAvailable() {
        // total memory unavailable on this JVM
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.empty());

        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.fixed.limit=10",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.timeout=500");

        // should revert to 10 fixed tickets
        doTestFallbackToFixedStrategy(provider, PACK, 10, 500);

        logListener.assertWarn("The total memory on this machine cannot be determined");
    }

    @Test
    public void testAdaptiveStrategyRevertsToFixedIfJvmMaxMemoryNotAvailable() {
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(8L * MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                // MAX_VALUE means no fixed JVM max memory available (probably will never happen on the JVMs we support)
                Long.MAX_VALUE,
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.fixed.limit=10",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                "throttle.resource.scm-hosting.adaptive.mem.reserved=1024",
                "throttle.resource.scm-hosting.adaptive.mem.webapp=jvmmax",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 10, 500);

        logListener.assertWarn("The maximum memory required by this JVM cannot be determined");
    }

    @Test
    public void testAdaptiveStrategyRevertsToFixedIfSystemMemoryInsufficientForWebappAndReserved() {
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                768 * MEM_1MB,
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.fixed.limit=10",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                "throttle.resource.scm-hosting.adaptive.mem.reserved=512",
                "throttle.resource.scm-hosting.adaptive.mem.webapp=jvmmax",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 10, 500);

        logListener.assertWarn("This machine's total memory cannot support the application memory requirements");
    }

    @Test
    public void testAdaptiveStrategyRevertsToFixedIfSystemMemorySufficientForWebappAndReservedButNotAnyTickets() {
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                500 * MEM_1MB,
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.fixed.limit=10",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=50",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                "throttle.resource.scm-hosting.adaptive.mem.reserved=500",
                "throttle.resource.scm-hosting.adaptive.mem.webapp=jvmmax",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 10, 500);

        logListener.assertWarn(
                "This machine's total memory cannot safely support the configured minimum number of tickets");
    }

    @Test
    public void testAdaptiveStrategyProvidesNoBackstopIfDisabled() {
        // no memory backstop means the configured limit.max (50) is what we get
        doTestAdaptiveStrategyForSearchProperty(50,
                "throttle.resource.scm-hosting.adaptive.mem.backstop.enabled=false");
    }

    @Test
    public void testAdaptiveStrategyUsesFallbackValuesIfBadData() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=one-hundred",
                "throttle.resource.scm-hosting.adaptive.limit.min=sixteen",
                "throttle.resource.scm-hosting.adaptive.limit.max=fifty",
                "throttle.resource.scm-hosting.adaptive.cpu.target=the-sweet-spot",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=lottery-ticket?",
                "throttle.resource.scm-hosting.timeout=one-hour");
        doTestAdaptiveStrategyWithAllFallbackValues(provider, PACK);

        logListener.assertWarn("Invalid ticket acquire timeout of 'one-hour' seconds");
        logListener.assertWarn("'sixteen' is invalid");
        logListener.assertWarn("'fifty' is invalid");
        logListener.assertWarn("Invalid adaptive throttling interval of 'one-hundred' specified");
        logListener.assertWarn("Invalid adaptive throttling target CPU load of 'the-sweet-spot' specified");
        logListener.assertWarn("Invalid expression 'lottery-ticket?'");
    }

    @Test
    public void testAdaptiveStrategyUsesFallbackValuesIfUnspecified() {
        ResourceThrottleStrategyProvider provider = create("throttle.resource.scm-hosting.strategy=adaptive");
        doTestAdaptiveStrategyWithAllFallbackValues(provider, PACK);
    }

    @Test
    public void testAdaptiveStrategyFallsBackToFixedIfMinEqualsMax() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=15",
                "throttle.resource.scm-hosting.adaptive.target.cpu=0.82",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 15, 500);

        logListener.assertWarn("Unable to set minimum tickets to 15. The maximum number of tickets configured is 15");
    }

    @Test
    public void testAdaptiveStrategyFallsBackToFixedIfMinLargerThanMax() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=15",
                "throttle.resource.scm-hosting.adaptive.limit.max=14",
                "throttle.resource.scm-hosting.adaptive.target.cpu=0.82",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 14, 500);

        logListener.assertWarn("Unable to set minimum tickets to");
    }

    @Test
    public void testAdaptiveStrategyUsesMinValueOfOneIfZero() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=0",
                "throttle.resource.scm-hosting.adaptive.limit.max=15",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.adaptive.cpu.base=0.12",
                "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.13",
                "throttle.resource.scm-hosting.timeout=500");

        doTestAdaptiveStrategy(provider, PACK, 1, 15, fallbackFixedTicketsFor(PACK),
                fallbackFixedTicketsFor(PACK), 500, 10, 0.82d,
                0.08d / Runtime.getRuntime().availableProcessors(), 0.13d);

        logListener.assertWarn("evaluated to an invalid value");
    }

    @Test
    public void testAdaptiveStrategyFallsBackToFixedIfMaxIsZero() {
        // min must be less than max but since min must also be greater than 0 and max is defaulted to 1 then the
        // config is invalid
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=12",
                "throttle.resource.scm-hosting.adaptive.limit.max=0",
                "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 1, 500);

        logListener.assertWarn("evaluated to an invalid value");
        logListener.assertWarn("Unable to set minimum tickets to");
    }

    @Test
    public void testAdaptiveStrategyFallsBackToFixedIfMaxIsNegative() {
        // min must be less than max but since min must also be greater than 0 and max is defaulted to 1 then the
        // config is invalid
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.interval=10",
                "throttle.resource.scm-hosting.adaptive.limit.min=12",
                "throttle.resource.scm-hosting.adaptive.limit.max=1-2",
                "throttle.resource.scm-hosting.adaptive.target.cpu=0.82",
                "throttle.resource.scm-hosting.timeout=500");

        doTestFallbackToFixedStrategy(provider, PACK, 1, 500);

        logListener.assertWarn("The maximum ticket limit expression '1-2' evaluated to an invalid value");
        logListener.assertWarn("Unable to set minimum tickets to 12");
    }

    @Test
    public void testAdaptiveSupportsExpressions() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting.strategy=adaptive",
                "throttle.resource.scm-hosting.adaptive.limit.min=3*5",
                "throttle.resource.scm-hosting.adaptive.limit.max=100/2",
                "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.12 / 8");

        doTestAdaptiveStrategy(provider, PACK, 15, 50, fallbackFixedTicketsFor(PACK),
                fallbackFixedTicketsFor(PACK), 300, 5, 0.75d, 0.12d / 8, 0.8d);
    }

    @Test
    public void testFixedStrategy() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command.strategy=fixed",
                "throttle.resource.scm-command.fixed.limit=20",
                "throttle.resource.scm-command.timeout=2");
        doTestFixedStrategy(provider, COMMAND, 20, 2);
    }

    @Test
    public void testFixedStrategyLegacyConfig() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command=12",
                "throttle.resource.scm-command.timeout=3");
        doTestFixedStrategy(provider, COMMAND, 12, 3);
    }

    @Test
    public void testFixedStrategyLegacyConfigSupportsExpression() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command=6*2",
                "throttle.resource.scm-command.timeout=3");
        doTestFixedStrategy(provider, COMMAND, 12, 3);
    }

    @Test
    public void testFixedStrategyBadData() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command.strategy=fixed",
                "throttle.resource.scm-command.fixed.limit=twenty",
                "throttle.resource.scm-command.timeout=two");
        doTestFixedStrategyAllFallbacks(provider, COMMAND);

        logListener.assertWarn("The fixed ticket limit expression 'twenty' is invalid");
        logListener.assertWarn("Invalid ticket acquire timeout of 'two' seconds");
    }

    @Test
    public void testFixedStrategyMissingData() {
        ResourceThrottleStrategyProvider provider = create("throttle.resource.scm-command.strategy=fixed");
        doTestFixedStrategyAllFallbacks(provider, COMMAND);
    }

    @Test
    public void testFixedDeprecatedProperty() {
        ResourceThrottleStrategyProvider provider = create("throttle.resource.scm-hosting=2");
        doTestFixedStrategy(provider, PACK, 2, FALLBACK_TICKET_TIMEOUT_SECONDS.get(PACK));
    }

    @Test
    public void testFixedStrategyUsingDeprecatedPropertyIgnoresExplicitNewLimitProperty() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting=2",
                "throttle.resource.scm-hosting.fixed.limit=3");
        doTestFixedStrategy(provider, PACK, 2, FALLBACK_TICKET_TIMEOUT_SECONDS.get(PACK));
    }

    @Test
    public void testFixedStrategyUsingDeprecatedPropertyIgnoresExplicitStrategyProperty() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting=2",
                "throttle.resource.scm-hosting.strategy=adaptive");
        doTestFixedStrategy(provider, PACK, 2, FALLBACK_TICKET_TIMEOUT_SECONDS.get(PACK));
    }

    @Test
    public void testFixedStrategyUsingDeprecatedPropertyIsIgnoredIfValueEmpty() {
        ResourceThrottleStrategyProvider provider = create("throttle.resource.scm-hosting=");
        doTestFixedStrategy(provider, PACK,
                saturatedCast(fallbackFixedTicketsFor(PACK)),
                FALLBACK_TICKET_TIMEOUT_SECONDS.get(PACK));
    }

    @Test
    public void testFixedStrategyUsingDeprecatedPropertyDefaultIfBadLimit() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-hosting=bad");
        doTestFixedStrategy(provider, PACK,
                saturatedCast(fallbackFixedTicketsFor(PACK)),
                FALLBACK_TICKET_TIMEOUT_SECONDS.get(PACK));

        logListener.assertWarn("The fixed ticket limit expression 'bad' is invalid");
    }

    @Test
    public void testFixedStrategySupportsExpressionInTicketLimit() {
        ResourceThrottleStrategyProvider provider = create(
                "throttle.resource.scm-command.strategy=fixed",
                "throttle.resource.scm-command.fixed.limit=2*10",
                "throttle.resource.scm-command.timeout=2");
        doTestFixedStrategy(provider, COMMAND, 20, 2);
    }

    private static void doTestAdaptiveStrategy(ResourceThrottleStrategyProvider provider, ThrottledResource resource,
                                               int expectedMinTickets, int expectedMaxTickets, int expectedNoCpuTickets,
                                               int expectedInitialTickets, int expectedTimeoutSeconds,
                                               int expectedUpdateIntervalSeconds, double expectedTargetCpu,
                                               double expectedTypicalCpuPerTicket,
                                               double expectedCpuHistoricalWeighting) {

        Map<ThrottledResource, ResourceThrottleStrategy> strategies = provider.getAll();
        assertNotNull(strategies);

        ResourceThrottleStrategy strategy = strategies.get(resource);
        assertNotNull(strategy);
        assertThat(strategy, instanceOf(AdaptiveResourceThrottleStrategy.class));

        AdaptiveResourceThrottleStrategy
                adaptive = (AdaptiveResourceThrottleStrategy) strategy;
        assertEquals(expectedMinTickets, adaptive.getMinTickets());
        assertEquals(expectedMaxTickets, adaptive.getMaxTickets());
        assertEquals(expectedNoCpuTickets, adaptive.getFallbackTickets());
        assertEquals(expectedTargetCpu, adaptive.getTargetCpuLoad(), 0.001d);
        assertEquals(expectedTypicalCpuPerTicket, adaptive.getTypicalCpuPerTicket(), 0.001d);
        assertEquals(expectedCpuHistoricalWeighting, adaptive.getCpuHistoricalWeighting(), 0.001d);
        assertEquals(expectedTimeoutSeconds, adaptive.getAcquireTimeoutSeconds());
        assertEquals(expectedUpdateIntervalSeconds * INTERVALS_MAX_WITHOUT_CPU, adaptive.getFallbackThresholdSeconds());
        assertEquals(expectedInitialTickets, adaptive.getTotalTickets());
        assertEquals(OptionalLong.of(expectedUpdateIntervalSeconds), adaptive.getUpdateInterval(TimeUnit.SECONDS));
    }

    private static void doTestAdaptiveStrategyWithAllFallbackValues(
            ResourceThrottleStrategyProvider provider,
            ThrottledResource resource) {
        int maxTickets = (int) round(FALLBACK_ADAPTIVE_CPU_SCALE_MAX_TICKETS * Runtime.getRuntime().availableProcessors());
        int minTickets = (int) round(FALLBACK_ADAPTIVE_CPU_SCALE_MIN_TICKETS * Runtime.getRuntime().availableProcessors());
        int noCpuTickets = fallbackFixedTicketsFor(resource);
        int timeoutSeconds = MoreObjects.firstNonNull(FALLBACK_TICKET_TIMEOUT_SECONDS.get(resource), 0);
        doTestAdaptiveStrategy(provider, resource, minTickets, maxTickets, noCpuTickets, noCpuTickets, timeoutSeconds,
                FALLBACK_ADAPTIVE_INTERVAL_SECONDS, FALLBACK_ADAPTIVE_TARGET_CPU_USAGE,
                FALLBACK_ADAPTIVE_CPU_PER_TICKET, FALLBACK_ADAPTIVE_CPU_HISTORICAL_WEIGHTING);
    }

    private static void doTestFixedStrategy(ResourceThrottleStrategyProvider provider, ThrottledResource resource,
                                            int expectedTickets, long expectedTimeoutSeconds) {

        Map<ThrottledResource, ResourceThrottleStrategy> strategies = provider.getAll();
        assertNotNull(strategies);

        ResourceThrottleStrategy strategy = strategies.get(resource);
        assertNotNull(strategy);
        assertThat(strategy, instanceOf(FixedResourceThrottleStrategy.class));

        FixedResourceThrottleStrategy
                fixed = (FixedResourceThrottleStrategy) strategy;
        assertEquals(expectedTickets, fixed.getTicketLimit());
        assertEquals(expectedTimeoutSeconds, fixed.getAcquireTimeoutSeconds());
        assertEquals(OptionalLong.empty(), fixed.getUpdateInterval(TimeUnit.MILLISECONDS));
    }

    private static Integer fallbackFixedTicketsFor(ThrottledResource resource) {
        return FALLBACK_FIXED_TICKETS.get(resource);
    }

    private void doTestFallbackToFixedStrategy(
            ResourceThrottleStrategyProvider provider, ThrottledResource resource,
            int expectedTickets, long expectedTimeoutSeconds) {
        doTestFixedStrategy(provider, resource, expectedTickets, expectedTimeoutSeconds);

        logListener.assertWarn("Selecting fixed throttling");
    }

    private static void doTestFixedStrategyAllFallbacks(
            ResourceThrottleStrategyProvider provider, ThrottledResource resource) {
        int ticketLimit = MoreObjects.firstNonNull(fallbackFixedTicketsFor(resource), 1);
        int timeoutSeconds = MoreObjects.firstNonNull(FALLBACK_TICKET_TIMEOUT_SECONDS.get(resource), 0);
        doTestFixedStrategy(provider, resource, ticketLimit, timeoutSeconds);
    }

    private ResourceThrottleStrategyProvider create(String... lines) {
        return create(DEFAULT_WEBAPP_MAX_MEM, lines);
    }

    private ResourceThrottleStrategyProvider create(long jvmMaxMemory, String... lines) {
        String config = StringUtils.join(lines, '\n');
        ByteArrayResource resource = new ByteArrayResource(config.getBytes(StandardCharsets.UTF_8));

        try {
            MockEnvironment environment = new MockEnvironment();
            environment.getPropertySources()
                    .addFirst(new PropertiesPropertySource("test", PropertiesLoaderUtils.loadProperties(resource)));

            ResourceThrottleStrategyProvider
                    provider = new ResourceThrottleStrategyProvider(clock, environment, operatingSystemService) {
                @Override
                OptionalLong getJvmMaxMemoryInBytes() {
                    return jvmMaxMemory == Long.MAX_VALUE ? OptionalLong.empty() : OptionalLong.of(jvmMaxMemory);
                }
            };
            provider.initialize();

            return provider;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void doTestAdaptiveStrategyForSearchProperty(int max, String... properties) {
        when(operatingSystemService.getTotalPhysicalMemory()).thenReturn(OptionalLong.of(8L * MEM_1GB));

        ResourceThrottleStrategyProvider provider = create(
                MEM_1GB,
                ImmutableList.<String>builder().add(
                        "throttle.resource.scm-hosting.strategy=adaptive",
                        "throttle.resource.scm-hosting.adaptive.interval=10",
                        "throttle.resource.scm-hosting.adaptive.limit.min=15",
                        "throttle.resource.scm-hosting.adaptive.limit.max=50",
                        "throttle.resource.scm-hosting.adaptive.cpu.target=0.82",
                        "throttle.resource.scm-hosting.adaptive.cpu.per.ticket=0.03",
                        "throttle.resource.scm-hosting.adaptive.cpu.historical.weighting=0.25",
                        "throttle.resource.scm-hosting.adaptive.mem.per.ticket=256",
                        "throttle.resource.scm-hosting.adaptive.mem.reserved=1024",
                        "throttle.resource.scm-hosting.timeout=500")
                        .add(properties)
                        .build().toArray(ObjectArrays.newArray(String.class, 11 + properties.length)));

        doTestAdaptiveStrategy(provider, PACK, 15, max, fallbackFixedTicketsFor(PACK),
                fallbackFixedTicketsFor(PACK), 500, 10, 0.82d, 0.03d, 0.25d);
    }
}