// Copyright 2017 The Chromium Authors
// 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 static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.process_launcher.ChildConnectionAllocator;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.TestChildProcessConnection;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;

/** Unit tests for the SpareChildConnection class. */
@Config(manifest = Config.NONE)
@RunWith(BaseRobolectricTestRunner.class)
@LooperMode(LooperMode.Mode.LEGACY)
public class SpareChildConnectionTest {
    @Mock
    private ChildProcessConnection.ServiceCallback mServiceCallback;

    // A connection allocator not used to create connections.
    private final ChildConnectionAllocator mWrongConnectionAllocator =
            ChildConnectionAllocator.createFixedForTesting(null, "org.chromium.test",
                    "TestServiceName", 3 /* serviceCount */, false /* bindToCaller */,
                    false /* bindAsExternalService */, false /* useStrongBinding */);

    // The allocator used to allocate the actual connection.
    private ChildConnectionAllocator mConnectionAllocator;

    private static class TestConnectionFactory
            implements ChildConnectionAllocator.ConnectionFactory {
        private TestChildProcessConnection mConnection;

        @Override
        public ChildProcessConnection createConnection(Context context, ComponentName serviceName,
                ComponentName fallbackServiceName, boolean bindToCaller,
                boolean bindAsExternalService, Bundle serviceBundle, String instanceName) {
            // We expect to create only one connection in these tests.
            assert mConnection == null;
            mConnection = new TestChildProcessConnection(
                    serviceName, bindToCaller, bindAsExternalService, serviceBundle);
            return mConnection;
        }

        public void simulateConnectionBindingSuccessfully() {
            mConnection.getServiceCallback().onChildStarted();
        }

        public void simulateConnectionFailingToBind() {
            mConnection.getServiceCallback().onChildStartFailed(mConnection);
        }

        public void simulateConnectionDied() {
            mConnection.getServiceCallback().onChildProcessDied(mConnection);
        }
    };

    private final TestConnectionFactory mTestConnectionFactory = new TestConnectionFactory();

    private SpareChildConnection mSpareConnection;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        // The tests run on only one thread. Pretend that is the launcher thread so LauncherThread
        // asserts are not triggered.
        LauncherThread.setCurrentThreadAsLauncherThread();

        mConnectionAllocator = ChildConnectionAllocator.createFixedForTesting(null,
                "org.chromium.test.spare_connection", "TestServiceName", 5 /* serviceCount */,
                false /* bindToCaller */, false /* bindAsExternalService */,
                false /* useStrongBinding */);
        mConnectionAllocator.setConnectionFactoryForTesting(mTestConnectionFactory);
        mSpareConnection = new SpareChildConnection(
                null /* context */, mConnectionAllocator, null /* serviceBundle */);
    }

    @After
    public void tearDown() {
        LauncherThread.setLauncherThreadAsLauncherThread();
    }

    /** Test creation and retrieval of connection. */
    @Test
    @Feature({"ProcessManagement"})
    public void testCreateAndGet() {
        // Tests retrieving the connection with the wrong allocator.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mWrongConnectionAllocator, mServiceCallback);
        assertNull(connection);

        // And with the right one.
        connection = mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNotNull(connection);

        // The connection has been used, subsequent calls should return null.
        connection = mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNull(connection);
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testCallbackNotCalledWhenNoConnection() {
        mTestConnectionFactory.simulateConnectionBindingSuccessfully();

        // Retrieve the wrong connection, no callback should be fired.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mWrongConnectionAllocator, mServiceCallback);
        assertNull(connection);
        ShadowLooper.runUiThreadTasks();
        verify(mServiceCallback, times(0)).onChildStarted();
        verify(mServiceCallback, times(0)).onChildStartFailed(any());
        verify(mServiceCallback, times(0)).onChildProcessDied(any());
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testCallbackCalledConnectionReady() {
        mTestConnectionFactory.simulateConnectionBindingSuccessfully();

        assertFalse(mSpareConnection.isEmpty());

        // Now retrieve the connection, the callback should be invoked.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNotNull(connection);

        // No more connections are available.
        assertTrue(mSpareConnection.isEmpty());

        ShadowLooper.runUiThreadTasks();
        verify(mServiceCallback, times(1)).onChildStarted();
        verify(mServiceCallback, times(0)).onChildStartFailed(any());
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testCallbackCalledConnectionNotReady() {
        assertFalse(mSpareConnection.isEmpty());

        // Retrieve the connection before it's bound.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNotNull(connection);
        ShadowLooper.runUiThreadTasks();
        // No callbacks are called.
        verify(mServiceCallback, times(0)).onChildStarted();
        verify(mServiceCallback, times(0)).onChildStartFailed(any());

        // No more connections are available.
        assertTrue(mSpareConnection.isEmpty());

        // Simulate the connection getting bound, it should trigger the callback.
        mTestConnectionFactory.simulateConnectionBindingSuccessfully();
        verify(mServiceCallback, times(1)).onChildStarted();
        verify(mServiceCallback, times(0)).onChildStartFailed(any());
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testUnretrievedConnectionFailsToBind() {
        mTestConnectionFactory.simulateConnectionFailingToBind();

        // We should not have a spare connection.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNull(connection);
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testRetrievedConnectionFailsToBind() {
        // Retrieve the spare connection before it's bound.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNotNull(connection);

        mTestConnectionFactory.simulateConnectionFailingToBind();

        // We should get a failure callback.
        verify(mServiceCallback, times(0)).onChildStarted();
        verify(mServiceCallback, times(1)).onChildStartFailed(connection);
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testRetrievedConnectionStops() {
        // Retrieve the spare connection before it's bound.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNotNull(connection);

        mTestConnectionFactory.simulateConnectionDied();

        // We should get a failure callback.
        verify(mServiceCallback, times(0)).onChildStarted();
        verify(mServiceCallback, times(0)).onChildStartFailed(any());
        verify(mServiceCallback, times(1)).onChildProcessDied(connection);
    }

    @Test
    @Feature({"ProcessManagement"})
    public void testConnectionFreeing() {
        // Simulate the connection dying.
        mTestConnectionFactory.simulateConnectionDied();

        // Connection should be gone.
        ChildProcessConnection connection =
                mSpareConnection.getConnection(mConnectionAllocator, mServiceCallback);
        assertNull(connection);
    }
}
