/*
 * Copyright (C) 2016 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.google.android.material.bottomnavigation;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.google.android.material.testutils.BottomNavigationViewActions.addMenuItem;
import static com.google.android.material.testutils.BottomNavigationViewActions.setIconForMenuItem;
import static com.google.android.material.testutils.BottomNavigationViewActions.setIconSize;
import static com.google.android.material.testutils.BottomNavigationViewActions.setItemIconTintList;
import static com.google.android.material.testutils.BottomNavigationViewActions.setLabelVisibilityMode;
import static com.google.android.material.testutils.BottomNavigationViewActions.showBadgeNumberForMenuItem;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.res.Resources;
import android.os.Build;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.material.navigation.NavigationBarItemView;
import com.google.android.material.navigation.NavigationBarMenuView;
import com.google.android.material.navigation.NavigationBarView;
import com.google.android.material.navigation.NavigationBarView.OnItemReselectedListener;
import com.google.android.material.navigation.NavigationBarView.OnItemSelectedListener;
import com.google.android.material.testapp.BottomNavigationViewActivity;
import com.google.android.material.testapp.R;
import com.google.android.material.testutils.AccessibilityUtils;
import com.google.android.material.testutils.TestDrawable;
import com.google.android.material.testutils.TestUtilsMatchers;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class BottomNavigationViewTest {
  @Rule
  public final ActivityTestRule<BottomNavigationViewActivity> activityTestRule =
      new ActivityTestRule<>(BottomNavigationViewActivity.class);

  private static final int[] MENU_CONTENT_ITEM_IDS = {
    R.id.destination_home, R.id.destination_profile, R.id.destination_people
  };
  private Map<Integer, String> menuStringContent;

  private BottomNavigationView bottomNavigation;

  @Before
  public void setUp() throws Exception {
    final BottomNavigationViewActivity activity = activityTestRule.getActivity();
    activity.setTheme(R.style.Theme_MaterialComponents_Light);
    bottomNavigation = activity.findViewById(R.id.bottom_navigation);

    final Resources res = activity.getResources();
    menuStringContent = new HashMap<>(MENU_CONTENT_ITEM_IDS.length);
    menuStringContent.put(R.id.destination_home, res.getString(R.string.navigate_home));
    menuStringContent.put(R.id.destination_profile, res.getString(R.string.navigate_profile));
    menuStringContent.put(R.id.destination_people, res.getString(R.string.navigate_people));
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testAddItemsWithoutMenuInflation() {
    BottomNavigationView navigation = new BottomNavigationView(activityTestRule.getActivity());
    activityTestRule.getActivity().setContentView(navigation);
    navigation.getMenu().add("Item1");
    navigation.getMenu().add("Item2");
    assertEquals(2, navigation.getMenu().size());
    navigation.getMenu().removeItem(0);
    navigation.getMenu().removeItem(0);
    assertEquals(0, navigation.getMenu().size());
  }

  @Test
  @SmallTest
  public void testBasics() {
    // Check the contents of the Menu object
    final Menu menu = bottomNavigation.getMenu();
    assertNotNull("Menu should not be null", menu);
    assertEquals("Should have matching number of items", MENU_CONTENT_ITEM_IDS.length, menu.size());
    for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) {
      final MenuItem currItem = menu.getItem(i);
      assertEquals("ID for Item #" + i, MENU_CONTENT_ITEM_IDS[i], currItem.getItemId());
    }
  }

  @Test
  @LargeTest
  public void testNavigationSelectionListener() {
    OnItemSelectedListener mockedListener = mock(OnItemSelectedListener.class);
    bottomNavigation.setOnItemSelectedListener(mockedListener);

    // Make the listener return true to allow selecting the item.
    when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true);
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_profile)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify our listener has been notified of the click
    verify(mockedListener, times(1))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_profile));
    // Verify the item is now selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Select the same item again
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_profile)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify our listener has been notified of the click
    verify(mockedListener, times(2))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_profile));
    // Verify the item is still selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Make the listener return false to disallow selecting the item.
    when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false);
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_people)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify our listener has been notified of the click
    verify(mockedListener, times(1))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_people));
    // Verify the previous item is still selected
    assertFalse(bottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Set null listener to test that the next click is not going to notify the
    // previously set listener and will allow selecting items.
    bottomNavigation.setOnItemSelectedListener(null);

    // Click one of our items
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_home)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify that our previous listener has not been notified of the click
    verifyNoMoreInteractions(mockedListener);
    // Verify the correct item is now selected.
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_home).isChecked());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testSetSelectedItemId() {
    OnItemSelectedListener mockedListener = mock(OnItemSelectedListener.class);
    bottomNavigation.setOnItemSelectedListener(mockedListener);

    // Make the listener return true to allow selecting the item.
    when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true);
    // Programmatically select an item
    bottomNavigation.setSelectedItemId(R.id.destination_profile);
    // Verify our listener has been notified of the click
    verify(mockedListener, times(1))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_profile));
    // Verify the item is now selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Select the same item
    bottomNavigation.setSelectedItemId(R.id.destination_profile);
    // Verify our listener has been notified of the click
    verify(mockedListener, times(2))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_profile));
    // Verify the item is still selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Make the listener return false to disallow selecting the item.
    when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false);
    // Programmatically select an item
    bottomNavigation.setSelectedItemId(R.id.destination_people);
    // Verify our listener has been notified of the click
    verify(mockedListener, times(1))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_people));
    // Verify the previous item is still selected
    assertFalse(bottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Set null listener to test that the next click is not going to notify the
    // previously set listener and will allow selecting items.
    bottomNavigation.setOnItemSelectedListener(null);

    // Select one of our items
    bottomNavigation.setSelectedItemId(R.id.destination_home);
    // Verify that our previous listener has not been notified of the click
    verifyNoMoreInteractions(mockedListener);
    // Verify the correct item is now selected.
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_home).isChecked());
  }

  @Test
  @SmallTest
  public void testNavigationReselectionListener() {
    // Add an OnItemReselectedListener
    OnItemReselectedListener reselectedListener =
        mock(NavigationBarView.OnItemReselectedListener.class);
    bottomNavigation.setOnItemReselectedListener(reselectedListener);

    // Select an item
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_profile)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify the item is now selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
    // Verify the listener was not called
    verify(reselectedListener, never()).onNavigationItemReselected(any(MenuItem.class));

    // Select the same item again
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_profile)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify the item is still selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());
    // Verify the listener was called
    verify(reselectedListener, times(1))
        .onNavigationItemReselected(bottomNavigation.getMenu().findItem(R.id.destination_profile));

    // Add an OnItemSelectedListener
    OnItemSelectedListener selectedListener = mock(OnItemSelectedListener.class);
    bottomNavigation.setOnItemSelectedListener(selectedListener);
    // Make the listener return true to allow selecting the item.
    when(selectedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true);

    // Select another item
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_people)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify the item is now selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
    // Verify the correct listeners were called
    verify(selectedListener, times(1))
        .onNavigationItemSelected(bottomNavigation.getMenu().findItem(R.id.destination_people));
    verify(reselectedListener, never())
        .onNavigationItemReselected(bottomNavigation.getMenu().findItem(R.id.destination_people));

    // Select the same item again
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_people)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify the item is still selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
    // Verify the correct listeners were called
    verifyNoMoreInteractions(selectedListener);
    verify(reselectedListener, times(1))
        .onNavigationItemReselected(bottomNavigation.getMenu().findItem(R.id.destination_people));

    // Remove the OnItemReselectedListener
    bottomNavigation.setOnItemReselectedListener(null);

    // Select the same item again
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_people)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Verify the item is still selected
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_people).isChecked());
    // Verify the reselectedListener was not called
    verifyNoMoreInteractions(reselectedListener);
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testSelectedItemIdWithEmptyMenu() {
    // First item initially selected
    assertEquals(R.id.destination_home, bottomNavigation.getSelectedItemId());

    // Remove all the items
    for (int id : menuStringContent.keySet()) {
      bottomNavigation.getMenu().removeItem(id);
    }
    // Verify selected ID is zero
    assertEquals(0, bottomNavigation.getSelectedItemId());

    // Add an item
    bottomNavigation.getMenu().add(0, R.id.destination_home, 0, R.string.navigate_home);
    // Verify item is selected
    assertEquals(R.id.destination_home, bottomNavigation.getSelectedItemId());

    // Try selecting an invalid ID
    bottomNavigation.setSelectedItemId(R.id.destination_people);
    // Verify the view has not changed
    assertEquals(R.id.destination_home, bottomNavigation.getSelectedItemId());
  }

  @Test
  @SmallTest
  public void testIconTinting() {
    final Resources res = activityTestRule.getActivity().getResources();
    @ColorInt final int redFill = ResourcesCompat.getColor(res, R.color.test_red, null);
    @ColorInt final int greenFill = ResourcesCompat.getColor(res, R.color.test_green, null);
    @ColorInt final int blueFill = ResourcesCompat.getColor(res, R.color.test_blue, null);
    final int iconSize = res.getDimensionPixelSize(R.dimen.drawable_small_size);
    onView(withId(R.id.bottom_navigation))
        .perform(
            setIconForMenuItem(
                R.id.destination_home, new TestDrawable(redFill, iconSize, iconSize)));
    onView(withId(R.id.bottom_navigation))
        .perform(
            setIconForMenuItem(
                R.id.destination_profile, new TestDrawable(greenFill, iconSize, iconSize)));
    onView(withId(R.id.bottom_navigation))
        .perform(
            setIconForMenuItem(
                R.id.destination_people, new TestDrawable(blueFill, iconSize, iconSize)));

    @ColorInt
    final int defaultTintColor = ResourcesCompat.getColor(res, R.color.emerald_translucent, null);

    // We're allowing a margin of error in checking the color of the items' icons.
    // This is due to the translucent color being used in the icon tinting
    // and off-by-one discrepancies of SRC_IN when it's compositing
    // translucent color. Note that all the checks below are written for the current
    // logic on BottomNavigationView that uses the default SRC_IN tint mode - effectively
    // replacing all non-transparent pixels in the destination (original icon) with
    // our translucent tint color.
    final int allowedComponentVariance = 1;

    // Note that here we're tying ourselves to the implementation details of the internal
    // structure of the BottomNavigationView. Specifically, we're checking the drawable the
    // ImageView with id R.id.navigation_bar_item_icon_view. If the internal implementation of
    // BottomNavigationView
    // changes, the second Matcher in the lookups below will need to be tweaked.
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_home))))
        .check(matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_profile))))
        .check(matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_people))))
        .check(matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance)));

    @ColorInt final int newTintColor = ResourcesCompat.getColor(res, R.color.red_translucent, null);
    onView(withId(R.id.bottom_navigation))
        .perform(
            setItemIconTintList(
                ResourcesCompat.getColorStateList(
                    res, R.color.color_state_list_red_translucent, null)));
    // Check that all menu items with icons now have icons tinted with the newly set color
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_home))))
        .check(matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_profile))))
        .check(matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_people))))
        .check(matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance)));

    // And now remove all icon tinting
    onView(withId(R.id.bottom_navigation)).perform(setItemIconTintList(null));
    // And verify that all menu items with icons now have the original colors for their icons.
    // Note that since there is no tinting at this point, we don't allow any color variance
    // in these checks.
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_home))))
        .check(matches(TestUtilsMatchers.drawable(redFill, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_profile))))
        .check(matches(TestUtilsMatchers.drawable(greenFill, allowedComponentVariance)));
    onView(
            allOf(
                withId(R.id.navigation_bar_item_icon_view),
                isDescendantOfA(withId(R.id.destination_people))))
        .check(matches(TestUtilsMatchers.drawable(blueFill, allowedComponentVariance)));
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testItemChecking() throws Throwable {
    final Menu menu = bottomNavigation.getMenu();
    assertTrue(menu.getItem(0).isChecked());
    checkAndVerifyExclusiveItem(menu, R.id.destination_home);
    checkAndVerifyExclusiveItem(menu, R.id.destination_profile);
    checkAndVerifyExclusiveItem(menu, R.id.destination_people);
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testAutoLabelVisibilityItemChecking() throws Throwable {
    bottomNavigation.getMenu().clear();
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_AUTO);
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_shifting_content);
    final Menu menu = bottomNavigation.getMenu();
    assertTrue(menu.getItem(0).isChecked());
    checkAndVerifyExclusiveItem(menu, R.id.destination_home);
    checkAndVerifyExclusiveItem(menu, R.id.destination_profile);
    checkAndVerifyExclusiveItem(menu, R.id.destination_people);
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testClearingMenu() throws Throwable {
    bottomNavigation.getMenu().clear();
    assertEquals(0, bottomNavigation.getMenu().size());
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_content);
    assertEquals(3, bottomNavigation.getMenu().size());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testClearingAutoMenu() throws Throwable {
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_AUTO);
    bottomNavigation.getMenu().clear();
    assertEquals(0, bottomNavigation.getMenu().size());
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_shifting_content);
    assertEquals(4, bottomNavigation.getMenu().size());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testClearingSelectedMenu() throws Throwable {
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_SELECTED);
    bottomNavigation.getMenu().clear();
    assertEquals(0, bottomNavigation.getMenu().size());
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_shifting_content);
    assertEquals(4, bottomNavigation.getMenu().size());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testClearingLabeledMenu() throws Throwable {
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED);
    bottomNavigation.getMenu().clear();
    assertEquals(0, bottomNavigation.getMenu().size());
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_content);
    assertEquals(3, bottomNavigation.getMenu().size());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testClearingUnlabeledMenu() throws Throwable {
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED);
    bottomNavigation.getMenu().clear();
    assertEquals(0, bottomNavigation.getMenu().size());
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_shifting_content);
    assertEquals(4, bottomNavigation.getMenu().size());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testSettingMenuItemVisibility() throws Throwable {
    final MenuItem homeMenuItem = bottomNavigation.getMenu().findItem(R.id.destination_home);
    assertTrue(homeMenuItem.isVisible());
    homeMenuItem.setVisible(false);
    assertFalse(homeMenuItem.isVisible());

    bottomNavigation.getMenu().clear();
    bottomNavigation.inflateMenu(R.menu.bottom_navigation_view_with_invisible_button_content);
    assertEquals(3, bottomNavigation.getMenu().size());

    final MenuItem destinationMenuItem =
        bottomNavigation.getMenu().findItem(R.id.destination_profile);
    assertFalse(destinationMenuItem.isVisible());
    destinationMenuItem.setVisible(true);
    assertTrue(destinationMenuItem.isVisible());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testSettingLabeledMenuItemVisibility() throws Throwable {
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED);
    final MenuItem homeMenuItem = bottomNavigation.getMenu().findItem(R.id.destination_home);
    assertTrue(homeMenuItem.isVisible());
    homeMenuItem.setVisible(false);
    assertFalse(homeMenuItem.isVisible());
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testSettingAutoItemVisibility() throws Throwable {
    bottomNavigation.getMenu().clear();
    bottomNavigation.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_AUTO);
    bottomNavigation.inflateMenu(
        R.menu.bottom_navigation_view_shifting_with_invisible_button_content);
    assertEquals(4, bottomNavigation.getMenu().size());

    final MenuItem destinationMenuItem =
        bottomNavigation.getMenu().findItem(R.id.destination_profile);
    assertFalse(destinationMenuItem.isVisible());
    destinationMenuItem.setVisible(true);
    assertTrue(destinationMenuItem.isVisible());
  }

  @Test
  @SmallTest
  public void testLabelVisibilityDefaultShouldNotShift() {
    // Check that there are 3 menu items, so that shifting mode should not be enabled.
    assertEquals(3, bottomNavigation.getMenu().size());

    // Ensure that the items do not shift when there are 3 or less items.
    onView(withId(R.id.destination_profile)).perform(click());
    checkLabelVisibilityIsLabeled();

    onView(withId(R.id.destination_people)).perform(click());
    checkLabelVisibilityIsLabeled();
  }

  @Test
  @SmallTest
  public void testLabelVisibilityDefaultShouldShift() {
    // Add a navigation item to trigger the default shifting behavior when there are more than 3
    // navigation items.
    onView(withId(R.id.bottom_navigation)).perform(addMenuItem("Settings"));
    assertEquals(4, bottomNavigation.getMenu().size());

    // Ensure that the items shift when there are more than 3 items.
    onView(withId(R.id.destination_profile)).perform(click());
    checkLabelVisibilityIsSelected();

    onView(withId(R.id.destination_people)).perform(click());
    checkLabelVisibilityIsSelected();
  }

  @Test
  @SmallTest
  public void testLabelVisibilityModeUnlabeledLabelsAreNotDisplayed() {
    onView(withId(R.id.bottom_navigation))
        .perform(setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED));
    onView(withId(R.id.destination_profile)).perform(click());

    checkLabelVisibilityIsUnlabeled();
  }

  @Test
  @SmallTest
  public void testLabelVisibilityModeLabeledLabelsAreDisplayed() {
    onView(withId(R.id.bottom_navigation))
        .perform(setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED));
    onView(withId(R.id.destination_profile)).perform(click());

    checkLabelVisibilityIsLabeled();
  }

  @Test
  @SmallTest
  public void testLabelVisibilityModeSelectedLabelsAreDisplayedWhenSelected() {
    onView(withId(R.id.bottom_navigation))
        .perform(setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_SELECTED));
    onView(withId(R.id.destination_profile)).perform(click());

    checkLabelVisibilityIsSelected();
  }

  @Test
  @SmallTest
  public void testIconSizeChanges() {
    int size = 32;
    onView(withId(R.id.bottom_navigation)).perform(setIconSize(size));
    assertEquals(size, bottomNavigation.getItemIconSize());

    // Change the size again to ensure that the size actually changes.
    size = 18;
    onView(withId(R.id.bottom_navigation)).perform(setIconSize(size));
    assertEquals(size, bottomNavigation.getItemIconSize());
  }

  @Test
  @SmallTest
  public void testSavedState() throws Throwable {
    // Select an item other than the first
    onView(
            allOf(
                withText(menuStringContent.get(R.id.destination_profile)),
                isDescendantOfA(withId(R.id.bottom_navigation)),
                isDisplayed()))
        .perform(click());
    // Show badge number on the first bottom navigation item view.
    onView(withId(R.id.bottom_navigation))
        .perform(showBadgeNumberForMenuItem(R.id.destination_home, 75));
    assertTrue(bottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked());

    // Save the current state
    SparseArray<Parcelable> container = new SparseArray<>();
    bottomNavigation.saveHierarchyState(container);

    // Restore the state into a fresh BottomNavigationView
    activityTestRule.runOnUiThread(
        () -> {
          BottomNavigationView testView = new BottomNavigationView(activityTestRule.getActivity());
          testView.setId(R.id.bottom_navigation);
          testView.inflateMenu(R.menu.bottom_navigation_view_content);
          testView.restoreHierarchyState(container);
          assertTrue(testView.getMenu().findItem(R.id.destination_profile).isChecked());

          assertTrue(
              ((NavigationBarMenuView) testView.getMenuView())
                  .findItemView(R.id.destination_home)
                  .getBadge()
                  .isVisible());

          assertTrue(testView.getBadge(R.id.destination_home).isVisible());
          assertEquals(75, testView.getBadge(R.id.destination_home).getNumber());
          assertEquals(4, testView.getBadge(R.id.destination_home).getMaxCharacterCount());
        });
  }

  @UiThreadTest
  @Test
  @SmallTest
  @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
  @TargetApi(Build.VERSION_CODES.N)
  public void testPointerIcon() throws Throwable {
    final Activity activity = activityTestRule.getActivity();
    final PointerIcon expectedIcon = PointerIcon.getSystemIcon(activity, PointerIcon.TYPE_HAND);
    final MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0);
    final Menu menu = bottomNavigation.getMenu();
    for (int i = 0; i < menu.size(); i++) {
      final MenuItem item = menu.getItem(i);
      assertTrue(item.isEnabled());
      final View itemView = activity.findViewById(item.getItemId());
      assertEquals(expectedIcon, itemView.onResolvePointerIcon(event, 0));
      item.setEnabled(false);
      assertEquals(null, itemView.onResolvePointerIcon(event, 0));
      item.setEnabled(true);
      assertEquals(expectedIcon, itemView.onResolvePointerIcon(event, 0));
    }
  }

  @UiThreadTest
  @Test
  @SmallTest
  public void testContentDescription() {
    ViewGroup menuView = (ViewGroup) bottomNavigation.getChildAt(0);
    final int count = menuView.getChildCount();
    for (int i = 0; i < count; i++) {
      View child = menuView.getChildAt(i);
      // We're using the same strings for content description
      assertEquals(menuStringContent.get(child.getId()), child.getContentDescription().toString());
    }

    menuView.getChildAt(0).getContentDescription();
  }

  @UiThreadTest
  @Test
  @SmallTest
  @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
  public void testOnInitializeAccessibilityNodeInfo() {
    NavigationBarMenuView menuView = (NavigationBarMenuView) bottomNavigation.getMenuView();

    AccessibilityNodeInfoCompat groupInfoCompat = AccessibilityNodeInfoCompat.obtain();
    ViewCompat.onInitializeAccessibilityNodeInfo(menuView, groupInfoCompat);

    CollectionInfoCompat collectionInfo = groupInfoCompat.getCollectionInfo();
    Assert.assertEquals(3, collectionInfo.getColumnCount());
    Assert.assertEquals(1, collectionInfo.getRowCount());

    NavigationBarItemView secondChild = (NavigationBarItemView) menuView.getChildAt(1);
    secondChild.setSelected(true);
    AccessibilityNodeInfoCompat buttonInfoCompat = AccessibilityNodeInfoCompat.obtain();
    ViewCompat.onInitializeAccessibilityNodeInfo(secondChild, buttonInfoCompat);

    // A tab that is currently selected won't be clickable by a11y
    assertFalse(buttonInfoCompat.isClickable());
    assertFalse(
        AccessibilityUtils.hasAction(buttonInfoCompat, AccessibilityActionCompat.ACTION_CLICK));

    CollectionItemInfoCompat itemInfo = buttonInfoCompat.getCollectionItemInfo();
    Assert.assertEquals(1, itemInfo.getColumnIndex());
    Assert.assertEquals(0, itemInfo.getRowIndex());
    assertTrue(itemInfo.isSelected());
  }

  private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable {
    menu.findItem(id).setChecked(true);
    for (int i = 0; i < menu.size(); i++) {
      final MenuItem item = menu.getItem(i);
      if (item.getItemId() == id) {
        assertTrue(item.isChecked());
      } else {
        assertFalse(item.isChecked());
      }
    }
  }

  private void checkLabelVisibilityIsLabeled() {
    for (int id : MENU_CONTENT_ITEM_IDS) {
      MenuItem item = bottomNavigation.getMenu().findItem(id);
      if (item.isChecked()) {
        checkLargeLabelIsShown(String.valueOf(item.getTitle()));
      } else {
        checkSmallLabelIsShown(String.valueOf(item.getTitle()));
      }
    }
  }

  private void checkLabelVisibilityIsUnlabeled() {
    for (int id : MENU_CONTENT_ITEM_IDS) {
      checkNoLabelIsShown(String.valueOf(bottomNavigation.getMenu().findItem(id).getTitle()));
    }
  }

  private void checkLabelVisibilityIsSelected() {
    for (int id : MENU_CONTENT_ITEM_IDS) {
      MenuItem item = bottomNavigation.getMenu().findItem(id);
      if (item.isChecked()) {
        checkLargeLabelIsShown(String.valueOf(item.getTitle()));
      } else {
        checkNoLabelIsShown(String.valueOf(item.getTitle()));
      }
    }
  }

  private void checkSmallLabelIsShown(String label) {
    onView(allOf(withId(R.id.navigation_bar_item_large_label_view), withText(label)))
        .check(matches(not(isDisplayed())));
    onView(allOf(withId(R.id.navigation_bar_item_small_label_view), withText(label)))
        .check(matches(isDisplayed()));
  }

  private void checkLargeLabelIsShown(String label) {
    onView(allOf(withId(R.id.navigation_bar_item_large_label_view), withText(label)))
        .check(matches(isDisplayed()));
    onView(allOf(withId(R.id.navigation_bar_item_small_label_view), withText(label)))
        .check(matches(not(isDisplayed())));
  }

  private void checkNoLabelIsShown(String label) {
    onView(allOf(withId(R.id.navigation_bar_item_large_label_view), withText(label)))
        .check(matches(not(isDisplayed())));
    onView(allOf(withId(R.id.navigation_bar_item_small_label_view), withText(label)))
        .check(matches(not(isDisplayed())));
  }
}
