/*
 * Copyright (C) 2020 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.collection.coordinator;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;

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;

@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class HeadsUpCoordinatorTest extends SysuiTestCase {

    private HeadsUpCoordinator mCoordinator;

    // captured listeners and pluggables:
    private NotifCollectionListener mCollectionListener;
    private NotifPromoter mNotifPromoter;
    private NotifLifetimeExtender mNotifLifetimeExtender;
    private OnHeadsUpChangedListener mOnHeadsUpChangedListener;
    private NotifSectioner mNotifSectioner;

    @Mock private NotifPipeline mNotifPipeline;
    @Mock private HeadsUpManager mHeadsUpManager;
    @Mock private HeadsUpViewBinder mHeadsUpViewBinder;
    @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
    @Mock private NotificationRemoteInputManager mRemoteInputManager;
    @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
    @Mock private NodeController mHeaderController;

    private NotificationEntry mEntry;

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

        mCoordinator = new HeadsUpCoordinator(
                mHeadsUpManager,
                mHeadsUpViewBinder,
                mNotificationInterruptStateProvider,
                mRemoteInputManager,
                mHeaderController);

        mCoordinator.attach(mNotifPipeline);

        // capture arguments:
        ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor =
                ArgumentCaptor.forClass(NotifCollectionListener.class);
        ArgumentCaptor<NotifPromoter> notifPromoterCaptor =
                ArgumentCaptor.forClass(NotifPromoter.class);
        ArgumentCaptor<NotifLifetimeExtender> notifLifetimeExtenderCaptor =
                ArgumentCaptor.forClass(NotifLifetimeExtender.class);
        ArgumentCaptor<OnHeadsUpChangedListener> headsUpChangedListenerCaptor =
                ArgumentCaptor.forClass(OnHeadsUpChangedListener.class);

        verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture());
        verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture());
        verify(mNotifPipeline).addNotificationLifetimeExtender(
                notifLifetimeExtenderCaptor.capture());
        verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture());

        mCollectionListener = notifCollectionCaptor.getValue();
        mNotifPromoter = notifPromoterCaptor.getValue();
        mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue();
        mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue();

        mNotifSectioner = mCoordinator.getSectioner();
        mNotifLifetimeExtender.setCallback(mEndLifetimeExtension);
        mEntry = new NotificationEntryBuilder().build();
    }

    @Test
    public void testPromotesCurrentHUN() {
        // GIVEN the current HUN is set to mEntry
        setCurrentHUN(mEntry);

        // THEN only promote the current HUN, mEntry
        assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry));
        assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build()));
    }

    @Test
    public void testIncludeInSectionCurrentHUN() {
        // GIVEN the current HUN is set to mEntry
        setCurrentHUN(mEntry);

        // THEN only section the current HUN, mEntry
        assertTrue(mNotifSectioner.isInSection(mEntry));
        assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder().build()));
    }

    @Test
    public void testLifetimeExtendsCurrentHUN() {
        // GIVEN there is a HUN, mEntry
        setCurrentHUN(mEntry);

        // THEN only the current HUN, mEntry, should be lifetimeExtended
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, /* cancellationReason */ 0));
        assertFalse(mNotifLifetimeExtender.shouldExtendLifetime(
                new NotificationEntryBuilder().build(), /* cancellationReason */ 0));
    }

    @Test
    public void testLifetimeExtensionEndsOnNewHUN() {
        // GIVEN there was a HUN that was lifetime extended
        setCurrentHUN(mEntry);
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
                mEntry, /* cancellation reason */ 0));

        // WHEN there's a new HUN
        NotificationEntry newHUN = new NotificationEntryBuilder().build();
        setCurrentHUN(newHUN);

        // THEN the old entry's lifetime extension should be cancelled
        verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
    }

    @Test
    public void testLifetimeExtensionEndsOnNoHUNs() {
        // GIVEN there was a HUN that was lifetime extended
        setCurrentHUN(mEntry);
        assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
                mEntry, /* cancellation reason */ 0));

        // WHEN there's no longer a HUN
        setCurrentHUN(null);

        // THEN the old entry's lifetime extension should be cancelled
        verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
    }

    @Test
    public void testShowHUNOnInflationFinished() {
        // WHEN a notification should HUN and its inflation is finished
        when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true);

        ArgumentCaptor<BindCallback> bindCallbackCaptor =
                ArgumentCaptor.forClass(BindCallback.class);
        mCollectionListener.onEntryAdded(mEntry);
        verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), bindCallbackCaptor.capture());

        bindCallbackCaptor.getValue().onBindFinished(mEntry);

        // THEN we tell the HeadsUpManager to show the notification
        verify(mHeadsUpManager).showNotification(mEntry);
    }

    @Test
    public void testNoHUNOnInflationFinished() {
        // WHEN a notification shouldn't HUN and its inflation is finished
        when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false);
        ArgumentCaptor<BindCallback> bindCallbackCaptor =
                ArgumentCaptor.forClass(BindCallback.class);
        mCollectionListener.onEntryAdded(mEntry);

        // THEN we never bind the heads up view or tell HeadsUpManager to show the notification
        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(
                eq(mEntry), bindCallbackCaptor.capture());
        verify(mHeadsUpManager, never()).showNotification(mEntry);
    }

    @Test
    public void testOnEntryRemovedRemovesHeadsUpNotification() {
        // GIVEN the current HUN is mEntry
        setCurrentHUN(mEntry);

        // WHEN mEntry is removed from the notification collection
        mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0);
        when(mRemoteInputManager.isSpinning(any())).thenReturn(false);

        // THEN heads up manager should remove the entry
        verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false);
    }

    private void setCurrentHUN(NotificationEntry entry) {
        when(mHeadsUpManager.getTopEntry()).thenReturn(entry);
        when(mHeadsUpManager.isAlerting(any())).thenReturn(false);
        if (entry != null) {
            when(mHeadsUpManager.isAlerting(entry.getKey())).thenReturn(true);
        }
        mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null);
    }
}
