// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.content.ComponentName;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.TestChildProcessConnection;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.content_public.browser.ChildProcessImportance;

/** Unit tests for ChildProessRanking */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ChildProcessRankingTest {
    private TestChildProcessConnection createConnection() {
        TestChildProcessConnection connection = new TestChildProcessConnection(
                new ComponentName("pkg", "cls"), false /* bindToCallerCheck */,
                false /* bindAsExternalService */, null /* serviceBundle */);
        connection.start(false /* useStrongBinding */, null /* serviceCallback */);
        return connection;
    }

    private void assertRankingAndRemoveAll(
            ChildProcessRanking ranking, ChildProcessConnection[] connections) {
        int index = connections.length;
        ChildProcessConnection reverseIterationArray[] =
                new ChildProcessConnection[connections.length];
        for (ChildProcessConnection c : ranking) {
            reverseIterationArray[--index] = c;
        }
        Assert.assertArrayEquals(connections, reverseIterationArray);
        Assert.assertEquals(0, index);

        index = connections.length;
        ChildProcessConnection reverseRemoveArray[] =
                new ChildProcessConnection[connections.length];
        for (int i = 0; i < connections.length; ++i) {
            ChildProcessConnection c = ranking.getLowestRankedConnection();
            reverseRemoveArray[--index] = c;
            ranking.removeConnection(c);
        }
        Assert.assertArrayEquals(connections, reverseRemoveArray);
        Assert.assertNull(ranking.getLowestRankedConnection());
    }

    private void assertNotInGroup(ChildProcessConnection[] connections) {
        for (ChildProcessConnection c : connections) {
            Assert.assertEquals(0, c.getGroup());
        }
    }

    private void assertInGroupOrderedByImportance(ChildProcessConnection[] connections) {
        int importanceSoFar = -1;
        for (ChildProcessConnection c : connections) {
            Assert.assertTrue(c.getGroup() > 0);
            Assert.assertTrue(c.getImportanceInGroup() > importanceSoFar);
            importanceSoFar = c.getImportanceInGroup();
        }
    }

    @Test
    public void testRanking() {
        ChildProcessRanking ranking = new ChildProcessRanking(10);
        doTestRanking(ranking, false);
    }

    @Test
    public void testRankingWithoutLimit() {
        ChildProcessRanking ranking = new ChildProcessRanking();
        doTestRanking(ranking, false);
    }

    @Test
    public void testEnableGroupAfter() {
        ChildProcessRanking ranking = new ChildProcessRanking();
        doTestRanking(ranking, true);
    }

    private void doTestRanking(ChildProcessRanking ranking, boolean enableGroupImportanceAfter) {
        if (!enableGroupImportanceAfter) ranking.enableServiceGroupImportance();

        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();
        ChildProcessConnection c4 = createConnection();
        ChildProcessConnection c5 = createConnection();
        ChildProcessConnection c6 = createConnection();
        ChildProcessConnection c7 = createConnection();
        ChildProcessConnection c8 = createConnection();
        ChildProcessConnection c9 = createConnection();
        ChildProcessConnection c10 = createConnection();


        // Insert in lowest ranked to highest ranked order.

        // Invisible frame.
        ranking.addConnection(c1, false /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        // Visible subframe outside viewport.
        ranking.addConnection(c2, true /* foreground */, 2 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c3, true /* foreground */, 1 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);

        // Visible subframe inside viewport.
        ranking.addConnection(c4, true /* foreground */, 2 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c5, true /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        // Visible main frame.
        ranking.addConnection(c6, true /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        if (enableGroupImportanceAfter) {
            assertNotInGroup(new ChildProcessConnection[] {c6, c5, c4, c3, c2, c1});
            ranking.enableServiceGroupImportance();
        }

        assertRankingAndRemoveAll(ranking, new ChildProcessConnection[] {c6, c5, c4, c3, c2, c1});

        assertNotInGroup(new ChildProcessConnection[] {c6, c5, c4});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c3, c2, c1});
    }

    @Test
    public void testRankingWithImportance() {
        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();
        ChildProcessConnection c4 = createConnection();

        ChildProcessRanking ranking = new ChildProcessRanking(4);
        ranking.enableServiceGroupImportance();

        // Insert in lowest ranked to highest ranked order.
        ranking.addConnection(c1, false /* foreground */, 0 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c2, false /* foreground */, 0 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.MODERATE);
        ranking.addConnection(c3, false /* foreground */, 1 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.IMPORTANT);
        ranking.addConnection(c4, false /* foreground */, 0 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.IMPORTANT);

        assertRankingAndRemoveAll(ranking, new ChildProcessConnection[] {c4, c3, c2, c1});
        assertNotInGroup(new ChildProcessConnection[] {c4, c3, c2});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c1});
    }

    @Test
    public void testUpdate() {
        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();
        ChildProcessConnection c4 = createConnection();

        ChildProcessRanking ranking = new ChildProcessRanking(4);
        ranking.enableServiceGroupImportance();

        // c1,2 are in one tab, and c3,4 are in second tab.
        ranking.addConnection(c1, true /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c2, true /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c3, false /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c4, false /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        Assert.assertEquals(c3, ranking.getLowestRankedConnection());

        // Switch from tab c1,2 to tab c3,c4.
        ranking.updateConnection(c1, false /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.updateConnection(c2, false /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.updateConnection(c3, true /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.updateConnection(c4, true /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        assertRankingAndRemoveAll(ranking, new ChildProcessConnection[] {c4, c3, c2, c1});
        assertNotInGroup(new ChildProcessConnection[] {c4, c3});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c2, c1});
    }

    @Test
    public void testIntersectsViewport() {
        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();

        ChildProcessRanking ranking = new ChildProcessRanking(4);
        ranking.enableServiceGroupImportance();

        // Insert in lowest ranked to highest ranked order.
        ranking.addConnection(c1, true /* foreground */, 1 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c2, true /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c3, true /* foreground */, 0 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        assertRankingAndRemoveAll(ranking, new ChildProcessConnection[] {c3, c2, c1});
        assertNotInGroup(new ChildProcessConnection[] {c3, c2});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c1});
    }

    @Test
    public void testFrameDepthIntOverflow() {
        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();
        ChildProcessRanking ranking = new ChildProcessRanking();

        // Native can pass up the maximum value of unsigned int.
        long intOverflow = ((long) Integer.MAX_VALUE) * 2;
        ranking.addConnection(c3, true /* foreground */, intOverflow - 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c2, true /* foreground */, 10 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c1, true /* foreground */, intOverflow /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);

        assertRankingAndRemoveAll(ranking, new ChildProcessConnection[] {c2, c3, c1});
    }

    @Test
    public void testThrowExceptionWhenGoingOverLimit() {
        ChildProcessRanking ranking = new ChildProcessRanking(2);

        ChildProcessConnection c1 = createConnection();
        ChildProcessConnection c2 = createConnection();
        ChildProcessConnection c3 = createConnection();

        ranking.addConnection(c1, true /* foreground */, 1 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c2, true /* foreground */, 1 /* frameDepth */,
                true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        boolean exceptionThrown = false;
        try {
            ranking.addConnection(c3, true /* foreground */, 1 /* frameDepth */,
                    true /* intersectsViewport */, ChildProcessImportance.NORMAL);
        } catch (Throwable e) {
            exceptionThrown = true;
        }
        Assert.assertTrue(exceptionThrown);
    }

    @Test
    public void testRebindHighRankConnection() {
        ChildProcessRanking ranking = new ChildProcessRanking();
        ranking.enableServiceGroupImportance();

        TestChildProcessConnection c1 = createConnection();
        TestChildProcessConnection c2 = createConnection();
        TestChildProcessConnection c3 = createConnection();

        ranking.addConnection(c1, true /* foreground */, 0 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.IMPORTANT);
        ranking.addConnection(c2, true /* foreground */, 2 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        ranking.addConnection(c3, true /* foreground */, 3 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);

        assertNotInGroup(new ChildProcessConnection[] {c1});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c2, c3});

        c1.getAndResetRebindCalled();
        ranking.updateConnection(c3, true /* foreground */, 1 /* frameDepth */,
                false /* intersectsViewport */, ChildProcessImportance.NORMAL);
        assertNotInGroup(new ChildProcessConnection[] {c1});
        assertInGroupOrderedByImportance(new ChildProcessConnection[] {c3, c2});
        Assert.assertFalse(c1.getAndResetRebindCalled());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertTrue(c1.getAndResetRebindCalled());
    }
}
