/*
 * Copyright (C) 2017 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 android.view.HapticFeedbackConstants.CLOCK_TICK;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.NotificationChannel;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.testing.ViewUtils;
import android.view.View;
import android.view.ViewGroup;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.Flags;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.utils.leaks.LeakCheckedTest;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

@RunWith(AndroidJUnit4.class)
@RunWithLooper
@SmallTest
public class NotificationMenuRowTest extends LeakCheckedTest {

    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
    private ExpandableNotificationRow mRow;
    private View mView;
    private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
    private NotificationActivityStarter mNotificationActivityStarter;

    @Before
    public void setup() {
        injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
        mRow = mock(ExpandableNotificationRow.class);
        mView = mock(View.class);
        mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class);
        mNotificationActivityStarter = mock(NotificationActivityStarter.class);
        NotificationEntry entry = new NotificationEntryBuilder()
                .setChannel(new NotificationChannel("hi", "hi", 2))
                .build();
        if (NotificationBundleUi.isEnabled()) {
            EntryAdapter entryAdapter = mKosmos.getEntryAdapterFactory().create(entry);
            when(mRow.getEntryAdapter()).thenReturn(entryAdapter);
        } else {
            when(mRow.getEntryLegacy()).thenReturn(entry);
        }

    }

    @Test
    public void testAttachDetach() {
        NotificationMenuRowPlugin row = new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter);
        row.createMenu(mRow);
        ViewUtils.attachView(row.getMenuView());
        TestableLooper.get(this).processAllMessages();
        ViewUtils.detachView(row.getMenuView());
        TestableLooper.get(this).processAllMessages();
    }

    @Test
    public void testRecreateMenu() {
        NotificationMenuRowPlugin row = new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter);
        row.createMenu(mRow);
        assertTrue(row.getMenuView() != null);
        row.createMenu(mRow);
        assertTrue(row.getMenuView() != null);
    }

    @Test
    public void testResetUncreatedMenu() {
        NotificationMenuRowPlugin row = new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter);
        row.resetMenu();
    }

    @Test
    public void testSlowSwipe_newDismiss() {
        when(mRow.getShowSnooze()).thenReturn(true);

        NotificationMenuRowPlugin row = new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter);
        row.createMenu(mRow);

        ViewGroup container = (ViewGroup) row.getMenuView();
        // Clear menu
        assertEquals(0, container.getChildCount());
    }

    @Test
    public void testIsSnappedAndOnSameSide() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));

        when(row.isMenuVisible()).thenReturn(true);
        when(row.isMenuSnapped()).thenReturn(true);
        when(row.isMenuOnLeft()).thenReturn(true);
        when(row.isMenuSnappedOnLeft()).thenReturn(true);

        assertTrue("Showing on left and on left", row.isSnappedAndOnSameSide());


        when(row.isMenuOnLeft()).thenReturn(false);
        when(row.isMenuSnappedOnLeft()).thenReturn(false);
        assertTrue("Snapped to right and on right", row.isSnappedAndOnSameSide());

        when(row.isMenuOnLeft()).thenReturn(true);
        when(row.isMenuSnapped()).thenReturn(false);
        assertFalse("Snapped to right and on left", row.isSnappedAndOnSameSide());

        when(row.isMenuOnLeft()).thenReturn(true);
        when(row.isMenuSnappedOnLeft()).thenReturn(true);
        when(row.isMenuVisible()).thenReturn(false);
        assertFalse("Snapped to left and on left, but menu not visible",
                row.isSnappedAndOnSameSide());

        when(row.isMenuVisible()).thenReturn(true);
        when(row.isMenuSnapped()).thenReturn(false);
        assertFalse("Snapped to left and on left, but not actually snapped to",
                row.isSnappedAndOnSameSide());
    }

    @Test
    public void testGetMenuSnapTarget() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        when(row.isMenuOnLeft()).thenReturn(true);
        doReturn(30).when(row).getSpaceForMenu();

        assertEquals("When on left, snap target is space for menu",
                30, row.getMenuSnapTarget());

        when(row.isMenuOnLeft()).thenReturn(false);
        assertEquals("When on right, snap target is negative space for menu",
                -30, row.getMenuSnapTarget());
    }

    @Test
    public void testIsSwipedEnoughToShowMenu() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        when(row.isMenuVisible()).thenReturn(true);
        when(row.isMenuOnLeft()).thenReturn(true);
        doReturn(40f).when(row).getMinimumSwipeDistance();

        when(row.getTranslation()).thenReturn(30f);
        assertFalse("on left, translation is less than min", row.isSwipedEnoughToShowMenu());

        when(row.getTranslation()).thenReturn(50f);
        assertTrue("on left, translation is greater than min", row.isSwipedEnoughToShowMenu());

        when(row.isMenuOnLeft()).thenReturn(false);
        when(row.getTranslation()).thenReturn(-30f);
        assertFalse("on right, translation is greater than -min", row.isSwipedEnoughToShowMenu());

        when(row.getTranslation()).thenReturn(-50f);
        assertTrue("on right, translation is less than -min", row.isSwipedEnoughToShowMenu());

        when(row.isMenuVisible()).thenReturn(false);
        when(row.getTranslation()).thenReturn(30f);
        assertFalse("on left, translation greater than min, but not visible",
                row.isSwipedEnoughToShowMenu());
    }

    @Test
    public void testIsWithinSnapMenuThreshold() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        doReturn(30f).when(row).getSnapBackThreshold();
        doReturn(50f).when(row).getDismissThreshold();
        doReturn(70).when(row).getSpaceForMenu();

        when(row.isMenuOnLeft()).thenReturn(true);
        when(row.getTranslation()).thenReturn(40f);
        assertTrue("When on left, translation is between min and max",
                row.isWithinSnapMenuThreshold());

        when(row.getTranslation()).thenReturn(20f);
        assertFalse("When on left, translation is less than min",
                row.isWithinSnapMenuThreshold());

        when(row.getTranslation()).thenReturn(60f);
        assertFalse("When on left, translation is greater than max",
                row.isWithinSnapMenuThreshold());

        when(row.isMenuOnLeft()).thenReturn(false);
        when(row.getTranslation()).thenReturn(-40f);
        assertTrue("When on right, translation is between -min and -max",
                row.isWithinSnapMenuThreshold());

        when(row.getTranslation()).thenReturn(-20f);
        assertFalse("When on right, translation is greater than -min",
                row.isWithinSnapMenuThreshold());

        when(row.getTranslation()).thenReturn(-60f);
        assertFalse("When on right, translation is less than -max",
                row.isWithinSnapMenuThreshold());
    }

    @Test
    public void testShouldSnapBack() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        doReturn(40f).when(row).getSnapBackThreshold();
        when(row.isMenuVisible()).thenReturn(false);
        when(row.isMenuOnLeft()).thenReturn(true);

        when(row.getTranslation()).thenReturn(50f);
        assertFalse("On left, translation greater than minimum target", row.shouldSnapBack());

        when(row.getTranslation()).thenReturn(30f);
        assertTrue("On left, translation less than minimum target", row.shouldSnapBack());

        when(row.isMenuOnLeft()).thenReturn(false);
        when(row.getTranslation()).thenReturn(-50f);
        assertFalse("On right, translation less than minimum target", row.shouldSnapBack());

        when(row.getTranslation()).thenReturn(-30f);
        assertTrue("On right, translation greater than minimum target", row.shouldSnapBack());
    }

    @Test
    public void testCanBeDismissed() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class);

        when(row.getParent()).thenReturn(parent);
        when(parent.canViewBeDismissed()).thenReturn(true);

        assertTrue("Row can be dismissed if parent can be dismissed", row.canBeDismissed());

        when(parent.canViewBeDismissed()).thenReturn(false);
        assertFalse("Row cannot be dismissed if parent cannot be dismissed",
                row.canBeDismissed());
    }

    @Test
    public void testIsTowardsMenu() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        when(row.isMenuVisible()).thenReturn(true);
        when(row.isMenuOnLeft()).thenReturn(true);

        assertTrue("menu on left, movement is negative", row.isTowardsMenu(-30f));
        assertFalse("menu on left, movement is positive", row.isTowardsMenu(30f));
        assertTrue("menu on left, movement is 0", row.isTowardsMenu(0f));

        when(row.isMenuOnLeft()).thenReturn(false);
        assertTrue("menu on right, movement is positive", row.isTowardsMenu(30f));
        assertFalse("menu on right, movement is negative", row.isTowardsMenu(-30f));
        assertTrue("menu on right, movement is 0", row.isTowardsMenu(0f));

        when(row.isMenuVisible()).thenReturn(false);
        assertFalse("menu on left, movement is negative, but menu not visible",
                row.isTowardsMenu(-30f));
    }

    @Test
    public void onSnapBack() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        NotificationMenuRowPlugin.OnMenuEventListener listener = mock(NotificationMenuRowPlugin
                .OnMenuEventListener.class);
        row.setMenuClickListener(listener);
        ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class);
        when(row.getParent()).thenReturn(parent);
        doNothing().when(row).cancelDrag();

        row.onSnapOpen();

        assertTrue("before onSnapClosed, row is snapped to", row.isMenuSnapped());
        assertFalse("before onSnapClosed, row is not snapping", row.isSnapping());

        row.onSnapClosed();

        assertFalse("after onSnapClosed, row is not snapped to", row.isMenuSnapped());
        assertTrue("after onSnapClosed, row is snapping", row.isSnapping());
    }

    @Test
    public void testOnSnap() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        when(row.isMenuOnLeft()).thenReturn(true);
        NotificationMenuRowPlugin.OnMenuEventListener listener = mock(NotificationMenuRowPlugin
                .OnMenuEventListener.class);
        row.setMenuClickListener(listener);
        ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class);
        when(row.getParent()).thenReturn(parent);

        assertFalse("before onSnapOpen, row is not snapped to", row.isMenuSnapped());
        assertFalse("before onSnapOpen, row is not snapped on left", row.isMenuSnappedOnLeft());

        row.onSnapOpen();

        assertTrue("after onSnapOpen, row is snapped to", row.isMenuSnapped());
        assertTrue("after onSnapOpen, row is snapped on left", row.isMenuSnapped());
        verify(listener, times(1)).onMenuShown(parent);
    }

    @Test
    public void testOnDismiss() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        doNothing().when(row).cancelDrag();
        row.onSnapOpen();

        assertFalse("before onDismiss, row is not dismissing", row.isDismissing());
        assertTrue("before onDismiss, row is showing", row.isMenuSnapped());

        row.onDismiss();

        verify(row, times(1)).cancelDrag();
        assertTrue("after onDismiss, row is dismissing", row.isDismissing());
        assertFalse("after onDismiss, row is not showing", row.isMenuSnapped());
    }

    @Test
    public void testOnDown() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        doNothing().when(row).beginDrag();

        row.onTouchStart();

        verify(row, times(1)).beginDrag();
    }

    @Test
    public void testOnUp() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        row.onTouchStart();

        assertTrue("before onTouchEnd, isUserTouching is true", row.isUserTouching());

        row.onTouchEnd();

        assertFalse("after onTouchEnd, isUserTouching is false", row.isUserTouching());
    }

    @Test
    public void testIsMenuVisible() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        row.setMenuAlpha(0);

        assertFalse("when alpha is 0, menu is not visible", row.isMenuVisible());

        row.setMenuAlpha(0.5f);
        assertTrue("when alpha is .5, menu is visible", row.isMenuVisible());
    }

    @DisableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES)
    @Test
    public void testOnTouchMove() {
        NotificationMenuRow row = Mockito.spy(new NotificationMenuRow(
                mContext, mPeopleNotificationIdentifier, mNotificationActivityStarter));
        row.createMenu(mRow);
        doReturn(50f).when(row).getDismissThreshold();
        doReturn(true).when(row).canBeDismissed();
        doReturn(mView).when(row).getMenuView();
        row.onTouchMove(30f);

        assertFalse("When moving not farther than threshold, menu is not snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(0)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(60f);

        assertTrue("When moving farther than threshold, menu is snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(1)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(70f);

        assertTrue("When moving farther than threshold, menu is snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(1)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(30f);

        assertFalse("When moving not farther than threshold, menu is not snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(2)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(-30f);

        assertFalse("When moving not farther than threshold, menu is not snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(2)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(-60f);

        assertTrue("When moving farther than threshold, menu is snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(3)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(-70f);

        assertTrue("When moving farther than threshold, menu is snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(3)).performHapticFeedback(CLOCK_TICK);

        row.onTouchMove(-30f);

        assertFalse("When moving not farther than threshold, menu is not snapping to dismiss",
                row.isSnappingToDismiss());
        verify(mView, times(4)).performHapticFeedback(CLOCK_TICK);
    }

    @Test
    @EnableFlags(Flags.FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING)
    public void testProhibitInlineSnooze() {
        NotificationMenuRowPlugin.MenuItem snoozeItem =
                NotificationMenuRow.createSnoozeItem(mContext);
        assertNull(snoozeItem.getMenuView());
    }

    @Test
    @DisableFlags(Flags.FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING)
    public void testAllowInlineSnooze() {
        NotificationMenuRowPlugin.MenuItem snoozeItem =
                NotificationMenuRow.createSnoozeItem(mContext);
        assertNotNull(snoozeItem.getMenuView());
    }
}
