/*
 * Copyright 2010-2013 Coda Hale and Yammer, Inc., 2014-2017 Dropwizard Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.dropwizard.metrics;

import org.junit.Test;

import io.dropwizard.metrics.ExponentiallyDecayingReservoir;
import io.dropwizard.metrics.Snapshot;

import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

public class ExponentiallyDecayingReservoirTest {
    @Test
    public void aReservoirOf100OutOf1000Elements() throws Exception {
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(100, 0.99);
        for (int i = 0; i < 1000; i++) {
            reservoir.update(i);
        }

        assertThat(reservoir.size())
                .isEqualTo(100);

        final Snapshot snapshot = reservoir.getSnapshot();

        assertThat(snapshot.size())
                .isEqualTo(100);

        assertAllValuesBetween(reservoir, 0, 1000);
    }

    @Test
    public void aReservoirOf100OutOf10Elements() throws Exception {
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(100, 0.99);
        for (int i = 0; i < 10; i++) {
            reservoir.update(i);
        }

        final Snapshot snapshot = reservoir.getSnapshot();

        assertThat(snapshot.size())
                .isEqualTo(10);

        assertThat(snapshot.size())
                .isEqualTo(10);

        assertAllValuesBetween(reservoir, 0, 10);
    }

    @Test
    public void aHeavilyBiasedReservoirOf100OutOf1000Elements() throws Exception {
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000, 0.01);
        for (int i = 0; i < 100; i++) {
            reservoir.update(i);
        }


        assertThat(reservoir.size())
                .isEqualTo(100);

        final Snapshot snapshot = reservoir.getSnapshot();

        assertThat(snapshot.size())
                .isEqualTo(100);

        assertAllValuesBetween(reservoir, 0, 100);
    }

    @Test
    public void longPeriodsOfInactivityShouldNotCorruptSamplingState() {
        final ManualClock clock = new ManualClock();
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(10,
                                                                                            0.015,
                                                                                            clock);

        // add 1000 values at a rate of 10 values/second
        for (int i = 0; i < 1000; i++) {
            reservoir.update(1000 + i);
            clock.addMillis(100);
        }
        assertThat(reservoir.getSnapshot().size())
                .isEqualTo(10);
        assertAllValuesBetween(reservoir, 1000, 2000);

        // wait for 15 hours and add another value.
        // this should trigger a rescale. Note that the number of samples will be reduced to 2
        // because of the very small scaling factor that will make all existing priorities equal to
        // zero after rescale.
        clock.addHours(15);
        reservoir.update(2000);
        assertThat(reservoir.getSnapshot().size())
                .isEqualTo(2);
        assertAllValuesBetween(reservoir, 1000, 3000);


        // add 1000 values at a rate of 10 values/second
        for (int i = 0; i < 1000; i++) {
            reservoir.update(3000 + i);
            clock.addMillis(100);
        }
        assertThat(reservoir.getSnapshot().size())
                .isEqualTo(10);
        assertAllValuesBetween(reservoir, 3000, 4000);
    }

    @Test
    public void spotLift() {
        final ManualClock clock = new ManualClock();
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000,
                                                                                            0.015,
                                                                                            clock);

        final int valuesRatePerMinute = 10;
        final int valuesIntervalMillis = (int) (TimeUnit.MINUTES.toMillis(1) / valuesRatePerMinute);
        // mode 1: steady regime for 120 minutes
        for (int i = 0; i < 120*valuesRatePerMinute; i++) {
            reservoir.update(177);
            clock.addMillis(valuesIntervalMillis);
        }
        
        // switching to mode 2: 10 minutes more with the same rate, but larger value
        for (int i = 0; i < 10*valuesRatePerMinute; i++) {
            reservoir.update(9999);
            clock.addMillis(valuesIntervalMillis);
        }
        
        // expect that quantiles should be more about mode 2 after 10 minutes
        assertThat(reservoir.getSnapshot().getMedian())
                .isEqualTo(9999);
    }

    @Test
    public void spotFall() {
        final ManualClock clock = new ManualClock();
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000,
                                                                                            0.015,
                                                                                            clock);
        
        final int valuesRatePerMinute = 10;
        final int valuesIntervalMillis = (int) (TimeUnit.MINUTES.toMillis(1) / valuesRatePerMinute);
        // mode 1: steady regime for 120 minutes
        for (int i = 0; i < 120*valuesRatePerMinute; i++) {
            reservoir.update(9998);
            clock.addMillis(valuesIntervalMillis);
        }
        
        // switching to mode 2: 10 minutes more with the same rate, but smaller value
        for (int i = 0; i < 10*valuesRatePerMinute; i++) {
            reservoir.update(178);
            clock.addMillis(valuesIntervalMillis);
        }
        
        // expect that quantiles should be more about mode 2 after 10 minutes
        assertThat(reservoir.getSnapshot().get95thPercentile())
                .isEqualTo(178);
    }
    
    @Test
    public void quantiliesShouldBeBasedOnWeights() {
        final ManualClock clock = new ManualClock();
        final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000,
                                                                                            0.015,
                                                                                            clock);
        for (int i = 0; i < 40; i++) {
            reservoir.update(177);
        }

        clock.addSeconds(120);
        
        for (int i = 0; i < 10; i++) {
            reservoir.update(9999);
        }

        assertThat(reservoir.getSnapshot().size())
                .isEqualTo(50);

        // the first added 40 items (177) have weights 1 
        // the next added 10 items (9999) have weights ~6 
        // so, it's 40 vs 60 distribution, not 40 vs 10
        assertThat(reservoir.getSnapshot().getMedian())
                .isEqualTo(9999);
        assertThat(reservoir.getSnapshot().get75thPercentile())
                .isEqualTo(9999);
    }
    
    private static void assertAllValuesBetween(ExponentiallyDecayingReservoir reservoir,
                                               double min,
                                               double max) {
        for (double i : reservoir.getSnapshot().getValues()) {
            assertThat(i)
                    .isLessThan(max)
                    .isGreaterThanOrEqualTo(min);
        }
    }

}
