/*
 * Copyright (C) 2019 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.systemui.statusbar.notification.row;

import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.testing.TestableLooper;

import androidx.annotation.NonNull;
import androidx.core.os.CancellationSignal;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

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

import java.util.ArrayList;
import java.util.List;

@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper
public class NotifBindPipelineTest extends SysuiTestCase {

    private NotifBindPipeline mBindPipeline;
    private final TestBindStage mStage = new TestBindStage();
    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());

    @Mock private NotificationEntry mEntry;
    @Mock private ExpandableNotificationRow mRow;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        CommonNotifCollection collection = mock(CommonNotifCollection.class);

        mBindPipeline = new NotifBindPipeline(
                collection,
                new NotifBindPipelineLogger(logcatLogBuffer()),
                new NotificationEntryProcessorFactoryExecutorImpl(mFakeExecutor));
        mBindPipeline.setStage(mStage);

        ArgumentCaptor<NotifCollectionListener> collectionListenerCaptor =
                ArgumentCaptor.forClass(NotifCollectionListener.class);
        verify(collection).addCollectionListener(collectionListenerCaptor.capture());
        NotifCollectionListener listener = collectionListenerCaptor.getValue();

        listener.onEntryInit(mEntry);
    }

    @Test
    public void testCallbackCalled() {
        // GIVEN a bound row
        mBindPipeline.manageRow(mEntry, mRow);

        // WHEN content is invalidated
        BindCallback callback = mock(BindCallback.class);
        mStage.requestRebind(mEntry, callback);
        mFakeExecutor.runAllReady();

        // WHEN stage finishes its work
        mStage.doWorkSynchronously();

        // THEN the callback is called when bind finishes
        verify(callback).onBindFinished(mEntry);
    }

    @Test
    public void testCallbackCancelled() {
        // GIVEN a bound row
        mBindPipeline.manageRow(mEntry, mRow);

        // GIVEN an in-progress pipeline run
        BindCallback callback = mock(BindCallback.class);
        CancellationSignal signal = mStage.requestRebind(mEntry, callback);
        mFakeExecutor.runAllReady();

        // WHEN the callback is cancelled.
        signal.cancel();

        // WHEN the stage finishes all its work
        mStage.doWorkSynchronously();

        // THEN the callback is not called when bind finishes
        verify(callback, never()).onBindFinished(mEntry);
    }

    @Test
    public void testMultipleCallbacks() {
        // GIVEN a bound row
        mBindPipeline.manageRow(mEntry, mRow);

        // WHEN the pipeline is invalidated.
        BindCallback callback = mock(BindCallback.class);
        mStage.requestRebind(mEntry, callback);
        mFakeExecutor.runAllReady();

        // WHEN the pipeline is invalidated again before the work completes.
        BindCallback callback2 = mock(BindCallback.class);
        mStage.requestRebind(mEntry, callback2);
        mFakeExecutor.runAllReady();

        // WHEN the stage finishes all work.
        mStage.doWorkSynchronously();

        // THEN both callbacks are called when the bind finishes
        verify(callback).onBindFinished(mEntry);
        verify(callback2).onBindFinished(mEntry);
    }

    /**
     * Bind stage for testing where asynchronous work can be synchronously controlled.
     */
    private static class TestBindStage extends BindStage {
        private List<Runnable> mExecutionRequests = new ArrayList<>();

        @Override
        protected void executeStage(@NonNull NotificationEntry entry,
                @NonNull ExpandableNotificationRow row, @NonNull StageCallback callback) {
            mExecutionRequests.add(() -> callback.onStageFinished(entry));
        }

        @Override
        protected void abortStage(@NonNull NotificationEntry entry,
                @NonNull ExpandableNotificationRow row) {

        }

        @Override
        protected Object newStageParams() {
            return null;
        }

        public void doWorkSynchronously() {
            for (Runnable work: mExecutionRequests) {
                work.run();
            }
        }
    }
}
