/*
 *
 *  Copyright 2016 Vladimir Bukhtoyarov
 *
 *    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 com.github.rollingmetrics.hitratio;

import com.github.rollingmetrics.util.Clock;
import org.junit.Test;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.*;


public class SmoothlyDecayingRollingHitRatioTest {

    private static int ROLLING_TIME_WINDOW_MILLIS = 5_000;
    private static int CHUNK_COUNT = 5;

    AtomicLong currentTimeMillis = new AtomicLong(0);
    Clock clock = Clock.mock(currentTimeMillis);
    HitRatio hitRatio = new SmoothlyDecayingRollingHitRatio(Duration.ofMillis(ROLLING_TIME_WINDOW_MILLIS), CHUNK_COUNT, clock);

    @Test
    public void testChunkRotation() {
        hitRatio.update(100, 100);
        assertEquals(1.0, hitRatio.getHitRatio(), 0.001);

        // switch to second chunk
        currentTimeMillis.set(1000);
        hitRatio.update(80, 100);
        assertEquals(0.9, hitRatio.getHitRatio(), 0.001);

        // switch to third chunk
        currentTimeMillis.set(2000);
        hitRatio.update(60, 100);
        assertEquals(0.8, hitRatio.getHitRatio(), 0.001);

        // switch to fourth chunk
        currentTimeMillis.set(3000);
        hitRatio.update(60, 100);
        assertEquals(0.75, hitRatio.getHitRatio(), 0.001);

        // switch to fifth chunk
        currentTimeMillis.set(4000);
        hitRatio.update(10, 100);
        assertEquals(0.62, hitRatio.getHitRatio(), 0.001);

        // switch to sixth chunk
        currentTimeMillis.set(5000);
        assertEquals(0.62, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(6000);
        // data of first chunk should be evicted
        assertEquals(0.525, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(7000);
        // data of second chunk should be evicted
        assertEquals(0.433, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(8000);
        // data of third chunk should be evicted
        assertEquals(0.35, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(9000);
        // data of fourth chunk should be evicted
        assertEquals(0.1, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(10_000);
        // data of fifth chunk should be evicted
        assertEquals(Double.NaN, hitRatio.getHitRatio(), 0.001);

        hitRatio.update(90, 1000);
        assertEquals(0.09, hitRatio.getHitRatio(), 0.001);
    }

    @Test
    public void testSmoothlyEvictionFromOldestChunk() {
        hitRatio.update(50, 100);
        assertEquals(0.5, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(1_000);
        hitRatio.update(100, 100);
        assertEquals(0.75, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(5_500);
        // oldest chunk should lost 50% of its weight
        assertEquals(0.833, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(5_750);
        // oldest chunk should lost 75% of its weight
        assertEquals(0.896, hitRatio.getHitRatio(), 0.001);

        currentTimeMillis.set(6_000);
        // oldest chunk should be fully invalidated
        assertEquals(1.0, hitRatio.getHitRatio(), 0.001);
    }

    @Test
    public void testHandlingArithmeticOverflow() {
        hitRatio.update(Integer.MAX_VALUE / 2, Integer.MAX_VALUE);
        assertEquals(0.5, hitRatio.getHitRatio(), 0.0001);

        hitRatio.update(0, Integer.MAX_VALUE);
        assertEquals(0.25, hitRatio.getHitRatio(), 0.0001);

        currentTimeMillis.set(1000);
        hitRatio.update(Integer.MAX_VALUE / 2, Integer.MAX_VALUE);
        assertEquals(0.375, hitRatio.getHitRatio(), 0.0001);

        hitRatio.update(0, Integer.MAX_VALUE);
        assertEquals(0.25, hitRatio.getHitRatio(), 0.0001);
    }

    @Test
    public void tesIllegalApiUsageDetection() {
        HitRationTestUtil.checkIllegalApiUsageDetection(hitRatio);
    }

    @Test(expected = IllegalArgumentException.class)
    public void tooShortTimeWindowShouldBeDisallowed() {
        new SmoothlyDecayingRollingHitRatio(Duration.ofMillis(SmoothlyDecayingRollingHitRatio.MIN_ROLLING_WINDOW_MILLIS - 1), 5);
    }

    @Test(expected = IllegalArgumentException.class)
    public void tooManyChunksShouldBeDisallowed() {
        new SmoothlyDecayingRollingHitRatio(Duration.ofMinutes(1), SmoothlyDecayingRollingHitRatio.MAX_CHUNKS + 1);
    }

    @Test
    public void getRollingWindow() throws Exception {
        SmoothlyDecayingRollingHitRatio hitRatio = new SmoothlyDecayingRollingHitRatio(Duration.ofMinutes(1), 6);
        assertEquals(Duration.ofMinutes(1), hitRatio.getRollingWindow());
    }

    @Test
    public void getChunkCount() throws Exception {
        SmoothlyDecayingRollingHitRatio hitRatio = new SmoothlyDecayingRollingHitRatio(Duration.ofMinutes(1), 6);
        assertEquals(6, hitRatio.getChunkCount());
    }

    @Test
    public void testToString() throws Exception {
        SmoothlyDecayingRollingHitRatio hitRatio = new SmoothlyDecayingRollingHitRatio(Duration.ofMinutes(1), 6);
        System.out.println(hitRatio.toString());
    }

    @Test(timeout = 32000)
    public void testThatConcurrentThreadsNotHung() throws InterruptedException {
        SmoothlyDecayingRollingHitRatio hitRatio = new SmoothlyDecayingRollingHitRatio(Duration.ofSeconds(1), 100);
        HitRationTestUtil.runInParallel(hitRatio, TimeUnit.SECONDS.toMillis(30));
    }

}