/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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.android.server.am;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.os.SystemClock;

import org.junit.Before;
import org.junit.Test;

/**
 * Test class for {@link DropboxRateLimiter}.
 *
 * Build/Install/Run:
 *  atest DropboxRateLimiterTest
 */
public class DropboxRateLimiterTest {
    private DropboxRateLimiter mRateLimiter;
    private TestClock mClock;

    @Before
    public void setUp() {
        mClock = new TestClock();
        mRateLimiter = new DropboxRateLimiter(mClock);
    }

    @Test
    public void testMultipleProcesses() {
        // The first 5 entries should not be rate limited.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        // Different processes and tags should not get rate limited either.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process2").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag2", "process").shouldRateLimit());
        // The 7th entry of the same process should be rate limited.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
    }

    @Test
    public void testBufferClearing() throws Exception {
        // The first 5 entries should not be rate limited.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        // The 7th entry of the same process should be rate limited.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // After 11 minutes there should be nothing left in the buffer and the same type of entry
        // should not get rate limited anymore.
        mClock.setOffsetMillis(11 * 60 * 1000);

        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
    }

    @Test
    public void testRecentlyDroppedCount() throws Exception {
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(1,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        assertEquals(2,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());

        // After 11 minutes the rate limiting buffer will be cleared and rate limiting will stop.
        mClock.setOffsetMillis(11 * 60 * 1000);

        // The first call after rate limiting stops will still return the number of dropped events.
        assertEquals(2,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
        // The next call should show that the dropped event counter was reset.
        assertEquals(0,
                mRateLimiter.shouldRateLimit("tag", "p").droppedCountSinceRateLimitActivated());
    }

    @Test
    public void testStrictRepeatedLimiting() throws Exception {
        // The first 6 entries should not be rate limited.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        // The 7th entry of the same process should be rate limited.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // After 11 minutes there should be nothing left in the buffer and the same type of entry
        // should not get rate limited anymore.
        mClock.setOffsetMillis(11 * 60 * 1000);
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        // The first 6 entries should not be rate limited again.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // The 7th entry of the same process should be rate limited.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // After 11 more minutes there should be nothing left in the buffer and the same type of
        // entry should not get rate limited anymore.
        mClock.setOffsetMillis(22 * 60 * 1000);
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // Repeated crashes after the last reset being rate limited should be restricted faster.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // We now need to wait 21 minutes for the buffer should be empty again.
        mClock.setOffsetMillis(43 * 60 * 1000);
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // After yet another 21 minutes, this time without triggering rate limiting, the strict
        // limiting should be turnd off.
        mClock.setOffsetMillis(64 * 60 * 1000);
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());

        // As rate limiting was not triggered in the last reset, after another 11 minutes the
        // buffer should still act as normal.
        mClock.setOffsetMillis(75 * 60 * 1000);
        // The first 6 entries should not be rate limited.
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        assertFalse(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
        // The 7th entry of the same process should be rate limited.
        assertTrue(mRateLimiter.shouldRateLimit("tag", "process").shouldRateLimit());
    }

    private static class TestClock implements DropboxRateLimiter.Clock {
        long mOffsetMillis = 0L;

        public long uptimeMillis() {
            return mOffsetMillis + SystemClock.uptimeMillis();
        }

        public void setOffsetMillis(long millis) {
            mOffsetMillis = millis;
        }
    }
}
