/*
 * Copyright (C) 2023 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.dreams;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.content.ComponentName;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.service.dreams.DreamOverlayService;
import android.service.dreams.IDreamOverlay;
import android.service.dreams.IDreamOverlayCallback;
import android.service.dreams.IDreamOverlayClient;
import android.service.dreams.IDreamOverlayClientCallback;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.util.concurrent.Executor;

/**
 * A collection of tests to exercise {@link DreamOverlayService}.
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DreamOverlayServiceTest {
    private static final ComponentName FIRST_DREAM_COMPONENT =
            ComponentName.unflattenFromString("com.foo.bar/.DreamService");
    private static final ComponentName SECOND_DREAM_COMPONENT =
            ComponentName.unflattenFromString("com.foo.baz/.DreamService");

    @Mock
    WindowManager.LayoutParams mLayoutParams;

    @Mock
    IDreamOverlayCallback mOverlayCallback;

    @Mock
    Executor mExecutor;

    /**
     * {@link TestDreamOverlayService} is a simple {@link DreamOverlayService} implementation for
     * tracking interactions across {@link IDreamOverlay} binder interface. The service reports
     * interactions to a {@link Monitor} instance provided at construction.
     */
    private static class TestDreamOverlayService extends DreamOverlayService {
        /**
         * An interface implemented to be informed when the corresponding methods in
         * {@link TestDreamOverlayService} are invoked.
         */
        interface Monitor {
            void onStartDream();
            void onEndDream();
            void onWakeUp();
        }

        private final Monitor mMonitor;

        TestDreamOverlayService(Monitor monitor, Executor executor) {
            super(executor);
            mMonitor = monitor;
        }

        @Override
        public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
            mMonitor.onStartDream();
        }

        @Override
        public void onEndDream() {
            mMonitor.onEndDream();
            super.onEndDream();
        }
    }

    /**
     * A {@link IDreamOverlayClientCallback} implementation that captures the requested client.
     */
    private static class OverlayClientCallback extends IDreamOverlayClientCallback.Stub {
        public IDreamOverlayClient retrievedClient;
        @Override
        public void onDreamOverlayClient(IDreamOverlayClient client) throws RemoteException {
            retrievedClient = client;
        }
    }

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

    /**
     * Verifies that callbacks for subclasses are run on the provided executor.
     */
    @Test
    @FlakyTest(bugId = 293108088)
    public void testCallbacksRunOnExecutor() throws RemoteException {
        final TestDreamOverlayService.Monitor monitor = Mockito.mock(
                TestDreamOverlayService.Monitor.class);
        final TestDreamOverlayService service = new TestDreamOverlayService(monitor, mExecutor);
        final IBinder binder = service.onBind(new Intent());
        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);

        final IDreamOverlayClient client = getClient(overlay);

        // Start the dream.
        client.startDream(mLayoutParams, mOverlayCallback,
                FIRST_DREAM_COMPONENT.flattenToString(), false);

        // The callback should not have run yet.
        verify(monitor, never()).onStartDream();

        // Run the Runnable sent to the executor.
        ArgumentCaptor<Runnable> mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
        verify(mExecutor).execute(mRunnableCaptor.capture());
        mRunnableCaptor.getValue().run();

        // Callback is run.
        verify(monitor).onStartDream();

        // Verify onWakeUp is run on the executor.
        client.wakeUp();
        verify(monitor, never()).onWakeUp();
        mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
        verify(mExecutor).execute(mRunnableCaptor.capture());
        mRunnableCaptor.getValue().run();
        verify(monitor).onWakeUp();

        // Verify onEndDream is run on the executor.
        client.endDream();
        verify(monitor, never()).onEndDream();
        mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
        verify(mExecutor).execute(mRunnableCaptor.capture());
        mRunnableCaptor.getValue().run();
        verify(monitor).onEndDream();
    }

    /**
     * Verifies that only the currently started dream is able to affect the overlay.
     */
    @Test
    public void testOverlayClientInteraction() throws RemoteException {
        doAnswer(invocation -> {
            ((Runnable) invocation.getArgument(0)).run();
            return null;
        }).when(mExecutor).execute(any());

        final TestDreamOverlayService.Monitor monitor = Mockito.mock(
                TestDreamOverlayService.Monitor.class);
        final TestDreamOverlayService service = new TestDreamOverlayService(monitor, mExecutor);
        final IBinder binder = service.onBind(new Intent());
        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);

        // Create two overlay clients and ensure they are unique.
        final IDreamOverlayClient firstClient = getClient(overlay);
        assertThat(firstClient).isNotNull();

        final IDreamOverlayClient secondClient = getClient(overlay);
        assertThat(secondClient).isNotNull();

        assertThat(firstClient).isNotEqualTo(secondClient);

        // Start a dream with the first client and ensure the dream is now active from the
        // overlay's perspective.
        firstClient.startDream(mLayoutParams, mOverlayCallback,
                FIRST_DREAM_COMPONENT.flattenToString(), false);


        verify(monitor).onStartDream();
        assertThat(service.getDreamComponent()).isEqualTo(FIRST_DREAM_COMPONENT);

        Mockito.clearInvocations(monitor);

        // Start a dream from the second client and verify that the overlay has both cycled to
        // the new dream (ended/started).
        secondClient.startDream(mLayoutParams, mOverlayCallback,
                SECOND_DREAM_COMPONENT.flattenToString(), false);

        verify(monitor).onEndDream();
        verify(monitor).onStartDream();
        assertThat(service.getDreamComponent()).isEqualTo(SECOND_DREAM_COMPONENT);

        Mockito.clearInvocations(monitor);

        // Verify that interactions with the first, now inactive client don't affect the overlay.
        firstClient.endDream();
        verify(monitor, never()).onEndDream();

        firstClient.wakeUp();
        verify(monitor, never()).onWakeUp();
    }

    private static IDreamOverlayClient getClient(IDreamOverlay overlay) throws RemoteException {
        final OverlayClientCallback callback = new OverlayClientCallback();
        overlay.getClient(callback);
        return callback.retrievedClient;
    }
}
