// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.widget;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;

import android.app.ActionBar.LayoutParams;
import android.app.Activity;
import android.graphics.Rect;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.PopupWindow;
import android.widget.TextView;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowPhoneWindow;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.ui.util.AccessibilityUtil;
import org.chromium.ui.widget.UiWidgetFactory;

/** Unit test for {@link ContextMenuDialog}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, shadows = {ShadowPhoneWindow.class})
@LooperMode(LooperMode.Mode.LEGACY)
public class ContextMenuDialogUnitTest {
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    ContextMenuDialog mDialog;

    Activity mActivity;
    FrameLayout mMenuContentView;
    View mRootView;

    @Mock
    UiWidgetFactory mMockUiWidgetFactory;
    @Spy
    PopupWindow mSpyPopupWindow;
    @Mock
    View mMockTouchEventDelegateView;
    @Mock
    AccessibilityUtil mMockAccessibilityUtil;

    @Before
    public void setup() {
        mActivity = Robolectric.buildActivity(Activity.class).setup().get();
        mRootView = new FrameLayout(mActivity);
        TextView textView = new TextView(mActivity);
        textView.setText("Test String");
        mMenuContentView = new FrameLayout(mActivity);
        mMenuContentView.addView(textView);
        mActivity.setContentView(
                mRootView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

        mSpyPopupWindow = Mockito.spy(UiWidgetFactory.getInstance().createPopupWindow(mActivity));
        UiWidgetFactory.setInstance(mMockUiWidgetFactory);
        Mockito.when(mMockUiWidgetFactory.createPopupWindow(any())).thenReturn(mSpyPopupWindow);
        Mockito.doNothing()
                .when(mSpyPopupWindow)
                .showAtLocation(any(View.class), anyInt(), anyInt(), anyInt());
        Mockito.doNothing().when(mSpyPopupWindow).dismiss();
    }

    @Test
    public void testCreate_usePopupStyle() {
        mDialog = createContextMenuDialog(/*isPopup=*/false, /*shouldRemoveScrim=*/true);
        mDialog.show();

        ShadowPhoneWindow window = (ShadowPhoneWindow) Shadows.shadowOf(mDialog.getWindow());
        Assert.assertTrue("FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS not in window flags.",
                window.getFlag(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS));
        Assert.assertTrue("FLAG_NOT_TOUCH_MODAL not in window flags.",
                window.getFlag(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL));
        Assert.assertFalse("FLAG_DIM_BEHIND is in flags.",
                window.getFlag(WindowManager.LayoutParams.FLAG_DIM_BEHIND));

        Assert.assertEquals("Dialog status bar color should match activity status bar color.",
                mActivity.getWindow().getStatusBarColor(), mDialog.getWindow().getStatusBarColor());
        Assert.assertEquals(
                "Dialog navigation bar color should match activity navigation bar color.",
                mActivity.getWindow().getNavigationBarColor(),
                mDialog.getWindow().getNavigationBarColor());
    }

    @Test
    public void testCreateDialog_useRegularStyle() {
        mDialog = createContextMenuDialog(/*isPopup=*/false, /*shouldRemoveScrim=*/false);
        mDialog.show();

        // Only checks the flag is unset to make sure the setup for |shouldRemoveScrim| is not ran.
        ShadowPhoneWindow window = (ShadowPhoneWindow) Shadows.shadowOf(mDialog.getWindow());
        Assert.assertFalse("FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is in window flags.",
                window.getFlag(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS));
        Assert.assertFalse("FLAG_NOT_TOUCH_MODAL is in window flags.",
                window.getFlag(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL));
    }

    @Test
    public void testShowPopupWindow() {
        mDialog = createContextMenuDialog(/*isPopup=*/true, /*shouldRemoveScrim=*/false);
        mDialog.show();
        requestLayoutForRootView();

        final ArgumentCaptor<Integer> gravityCaptor = ArgumentCaptor.forClass(Integer.class);
        Mockito.verify(mSpyPopupWindow)
                .showAtLocation(
                        eq(mRootView.getRootView()), gravityCaptor.capture(), anyInt(), anyInt());

        Assert.assertEquals("Popup gravity should have Gravity.START.", Gravity.START,
                (gravityCaptor.getValue() & Gravity.START));
        Assert.assertEquals("Popup gravity should have Gravity.TOP.", Gravity.TOP,
                (gravityCaptor.getValue() & Gravity.TOP), Gravity.TOP);

        mDialog.dismiss();
        Mockito.verify(mSpyPopupWindow).dismiss();
    }

    @Test
    public void testShowPopupWindow_2ndLayout() {
        mDialog = createContextMenuDialog(/*isPopup=*/true, /*shouldRemoveScrim=*/false);
        mDialog.show();
        // Change layout params and request layout so #onLayoutChange is triggered.
        requestLayoutForRootView();
        Mockito.verify(mSpyPopupWindow)
                .showAtLocation(eq(mRootView.getRootView()), anyInt(), anyInt(), anyInt());

        // Mock up popup window is showing.
        Mockito.doReturn(true).when(mSpyPopupWindow).isShowing();

        requestLayoutForRootView();
        Mockito.verify(mSpyPopupWindow).dismiss();
    }

    /**
     * Inspired by https://crbug.com/1281011. If popup context menu is dismissed before
     * #onLayoutRequest for the root view, popup menu should not get invoked.
     */
    @Test
    public void testShowPopupWindow_BeforeOnLayout() {
        mDialog = createContextMenuDialog(/*isPopup=*/true, /*shouldRemoveScrim=*/false);
        mDialog.show();

        mDialog.dismiss();
        // Spy popup is not invoked because the dialog does not manage to create the popup window.
        Mockito.verify(mSpyPopupWindow, Mockito.times(0)).dismiss();
    }

    @Test
    public void testShowPopupWindow_NotFocusableInA11y() throws Exception {
        Mockito.doReturn(true).when(mMockAccessibilityUtil).isTouchExplorationEnabled();

        mDialog = createContextMenuDialog(/*isPopup=*/true, /*shouldRemoveScrim=*/false);
        mDialog.show();
        // Change layout params and request layout so #onLayoutChange is triggered.
        requestLayoutForRootView();

        Mockito.verify(mSpyPopupWindow).setFocusable(eq(true));
    }

    @Test
    public void testDispatchTouchToDelegate() {
        mDialog = createContextMenuDialog(/*isPopup=*/true, /*shouldRemoveScrim=*/true);
        mDialog.show();
        requestLayoutForRootView();
        Mockito.verify(mSpyPopupWindow)
                .showAtLocation(eq(mRootView.getRootView()), anyInt(), anyInt(), anyInt());

        // common motion events other than ACTION_DOWN should be forwarded to touch event delegate.
        int[] motionEvenActions = new int[] {MotionEvent.ACTION_CANCEL,
                MotionEvent.ACTION_HOVER_ENTER, MotionEvent.ACTION_HOVER_EXIT,
                MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_OUTSIDE,
                MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP,
                MotionEvent.ACTION_SCROLL, MotionEvent.ACTION_UP};
        for (int actionType : motionEvenActions) {
            MotionEvent event = createMockMotionEventWithActionType(actionType);
            mDialog.onTouchEvent(event);
            Mockito.verify(mMockTouchEventDelegateView).dispatchTouchEvent(eq(event));
        }

        // ACTION_DOWN should dismiss the dialog and the popup window.
        MotionEvent downEvent = createMockMotionEventWithActionType(MotionEvent.ACTION_DOWN);
        mDialog.onTouchEvent(downEvent);
        Mockito.verify(mMockTouchEventDelegateView, Mockito.times(0))
                .dispatchTouchEvent(eq(downEvent));
        Mockito.verify(mSpyPopupWindow).dismiss();
    }

    private ContextMenuDialog createContextMenuDialog(boolean isPopup, boolean shouldRemoveScrim) {
        return new ContextMenuDialog(mActivity, 0, ContextMenuDialog.NO_CUSTOM_MARGIN,
                ContextMenuDialog.NO_CUSTOM_MARGIN, mRootView, mMenuContentView, isPopup,
                shouldRemoveScrim, 0, 0, mMockTouchEventDelegateView, new Rect(0, 0, 0, 0),
                mMockAccessibilityUtil);
    }

    private void requestLayoutForRootView() {
        // Change layout params and request layout so #onLayoutChange is triggered.
        mRootView.setRight(mRootView.getRight() + 1);
        mRootView.requestLayout();
    }

    private MotionEvent createMockMotionEventWithActionType(int actionType) {
        MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
        Mockito.doReturn(actionType).when(motionEvent).getAction();

        return motionEvent;
    }
}
