/*
 * 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.provider.Settings.Global.SHOW_NEW_NOTIF_DISMISS;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
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.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.testing.ViewUtils;
import android.view.View;
import android.view.ViewGroup;

import androidx.test.filters.SmallTest;

import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
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.utils.leaks.LeakCheckedTest;

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

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

    private ExpandableNotificationRow mRow;
    private View mView;
    private PeopleNotificationIdentifier mPeopleNotificationIdentifier;

    @Before
    public void setup() {
        injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
        mRow = mock(ExpandableNotificationRow.class);
        mView = mock(View.class);
        mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class);
        NotificationEntry entry = new NotificationEntryBuilder().build();
        when(mRow.getEntry()).thenReturn(entry);
    }

    @Test
    public void testAttachDetach() {
        NotificationMenuRowPlugin row =
                new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
        row.createMenu(mRow, null);
        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);
        row.createMenu(mRow, null);
        assertTrue(row.getMenuView() != null);
        row.createMenu(mRow, null);
        assertTrue(row.getMenuView() != null);
    }

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


    @Test
    public void testNoAppOpsInSlowSwipe() {
        when(mRow.getShowSnooze()).thenReturn(false);
        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);

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

        ViewGroup container = (ViewGroup) row.getMenuView();
        // noti blocking
        assertEquals(1, container.getChildCount());
    }

    @Test
    public void testNoSnoozeInSlowSwipe() {
        when(mRow.getShowSnooze()).thenReturn(false);
        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);

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

        ViewGroup container = (ViewGroup) row.getMenuView();
        // just for noti blocking
        assertEquals(1, container.getChildCount());
    }

    @Test
    public void testSnoozeInSlowSwipe() {
        when(mRow.getShowSnooze()).thenReturn(true);
        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);

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

        ViewGroup container = (ViewGroup) row.getMenuView();
        // one for snooze and one for noti blocking
        assertEquals(2, container.getChildCount());
    }

    @Test
    public void testSlowSwipe_newDismiss() {
        when(mRow.getShowSnooze()).thenReturn(true);
        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 1);

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

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

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

        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));
        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));
        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));
        doReturn(30f).when(row).getSnapBackThreshold();
        doReturn(50f).when(row).getDismissThreshold();

        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));
        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));
        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));
        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));
        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));
        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));
        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));
        doNothing().when(row).beginDrag();

        row.onTouchStart();

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

    @Test
    public void testOnUp() {
        NotificationMenuRow row = Mockito.spy(
                new NotificationMenuRow(mContext, mPeopleNotificationIdentifier));
        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));
        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());
    }

    @Test
    public void testOnTouchMove() {
        NotificationMenuRow row = Mockito.spy(
                new NotificationMenuRow(mContext, mPeopleNotificationIdentifier));
        row.createMenu(mRow, null);
        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);
    }
}
