chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListMediatorUnitTest.java

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

package org.chromium.chrome.browser.tasks.tab_management;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.tasks.tab_management.TabGroupRowProperties.COLOR_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabGroupRowProperties.DELETE_RUNNABLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGroupRowProperties.LEAVE_RUNNABLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGroupRowProperties.OPEN_RUNNABLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGroupRowProperties.TITLE_DATA;

import androidx.core.util.Pair;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
import org.chromium.base.Token;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.hub.PaneManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_group_sync.TabGroupUiActionHandler;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.ActionConfirmationManager.ConfirmationResult;
import org.chromium.components.data_sharing.DataSharingService;
import org.chromium.components.data_sharing.DataSharingService.GroupDataOrFailureOutcome;
import org.chromium.components.data_sharing.GroupData;
import org.chromium.components.data_sharing.GroupMember;
import org.chromium.components.data_sharing.PeopleGroupActionFailure;
import org.chromium.components.data_sharing.member_role.MemberRole;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.sync.DataType;
import org.chromium.components.sync.SyncService;
import org.chromium.components.tab_group_sync.LocalTabGroupId;
import org.chromium.components.tab_group_sync.SavedTabGroup;
import org.chromium.components.tab_group_sync.SavedTabGroupTab;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.components.tab_group_sync.TriggerSource;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.MockitoHelper;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Tests for {@link TabGroupListMediator}. */
@RunWith(BaseRobolectricTestRunner.class)
@DisableFeatures(ChromeFeatureList.DATA_SHARING)
@EnableFeatures(ChromeFeatureList.TAB_GROUP_PARITY_ANDROID)
public class TabGroupListMediatorUnitTest {
    private static final String SYNC_GROUP_ID1 = "remote one";
    private static final String SYNC_GROUP_ID2 = "remote two";
    private static final String SYNC_GROUP_ID3 = "remote three";
    private static final String COLLABORATION_ID1 = "A";
    private static final String GAIA_ID1 = "Z";
    private static final String GAIA_ID2 = "Y";
    private static final String EMAIL = "[email protected]";
    private static final Token LOCAL_GROUP_ID1 = new Token(1, 1);
    private static final Token LOCAL_GROUP_ID2 = new Token(2, 2);

    private static final int ROOT_ID1 = 1;
    private static final int ROOT_ID2 = 2;

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private TabGroupModelFilter mTabGroupModelFilter;
    @Mock private TabModel mTabModel;
    @Mock private TabList mComprehensiveModel;
    @Mock private TabGroupSyncService mTabGroupSyncService;
    @Mock private DataSharingService mDataSharingService;
    @Mock private IdentityManager mIdentityManager;
    @Mock private PaneManager mPaneManager;
    @Mock private FaviconResolver mFaviconResolver;
    @Mock private TabSwitcherPaneBase mTabSwitcherPaneBase;
    @Mock private TabGroupUiActionHandler mTabGroupUiActionHandler;
    @Mock private ActionConfirmationManager mActionConfirmationManager;
    @Mock private SyncService mSyncService;
    @Mock private Tab mTab1;
    @Mock private Tab mTab2;

    @Captor private ArgumentCaptor<TabModelObserver> mTabModelObserver;
    @Captor private ArgumentCaptor<TabGroupSyncService.Observer> mTabGroupSyncObserverCaptor;
    @Captor private ArgumentCaptor<Callback<Integer>> mConfirmationResultCallbackCaptor;

    @Captor
    private ArgumentCaptor<SyncService.SyncStateChangedListener> mSyncStateChangedListenerCaptor;

    @Captor private ArgumentCaptor<Callback<GroupDataOrFailureOutcome>> mReadGroupCallbackCaptor;

    private PropertyModel mPropertyModel;
    private ModelList mModelList;

    @Before
    public void setUp() {
        mPropertyModel = new PropertyModel(TabGroupListProperties.ALL_KEYS);
        mModelList = new ModelList();
        when(mPaneManager.getPaneForId(PaneId.TAB_SWITCHER)).thenReturn(mTabSwitcherPaneBase);
        when(mTabSwitcherPaneBase.requestOpenTabGroupDialog(anyInt())).thenReturn(true);
        when(mTabGroupModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabModel.getComprehensiveModel()).thenReturn(mComprehensiveModel);
    }

    private TabGroupListMediator createMediator() {
        return new TabGroupListMediator(
                mModelList,
                mPropertyModel,
                mTabGroupModelFilter,
                mFaviconResolver,
                mTabGroupSyncService,
                mDataSharingService,
                mIdentityManager,
                mPaneManager,
                mTabGroupUiActionHandler,
                mActionConfirmationManager,
                mSyncService);
    }

    @Test
    public void testNoTabGroups() {
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        createMediator();
        assertEquals(0, mModelList.size());
    }

    @Test
    public void testNoTabGroupSyncService() {
        mTabGroupSyncService = null;
        createMediator();
        assertEquals(0, mModelList.size());
    }

    @Test
    public void testOneGroup() {
        SavedTabGroup group = new SavedTabGroup();
        group.syncId = SYNC_GROUP_ID1;
        group.title = "Title";
        group.color = TabGroupColorId.BLUE;
        group.savedTabs = Arrays.asList(new SavedTabGroupTab());
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group);

        createMediator();
        assertEquals(1, mModelList.size());

        PropertyModel model = mModelList.get(0).model;
        assertEquals(new Pair<>("Title", 1), model.get(TITLE_DATA));
        assertEquals(TabGroupColorId.BLUE, model.get(COLOR_INDEX));
    }

    @Test
    public void testTwoGroups() {
        SavedTabGroup fooGroup = new SavedTabGroup();
        fooGroup.syncId = SYNC_GROUP_ID1;
        fooGroup.title = "Foo";
        fooGroup.color = TabGroupColorId.BLUE;
        fooGroup.savedTabs = Arrays.asList(new SavedTabGroupTab(), new SavedTabGroupTab());
        fooGroup.creationTimeMs = 1;

        SavedTabGroup barGroup = new SavedTabGroup();
        barGroup.syncId = SYNC_GROUP_ID1;
        barGroup.title = "Bar";
        barGroup.color = TabGroupColorId.RED;
        barGroup.savedTabs =
                Arrays.asList(
                        new SavedTabGroupTab(), new SavedTabGroupTab(), new SavedTabGroupTab());
        barGroup.creationTimeMs = 2;

        when(mTabGroupSyncService.getAllGroupIds())
                .thenReturn(new String[] {SYNC_GROUP_ID1, SYNC_GROUP_ID2});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(fooGroup);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2)).thenReturn(barGroup);

        createMediator();
        assertEquals(2, mModelList.size());

        PropertyModel barModel = mModelList.get(0).model;
        assertEquals(new Pair<>("Bar", 3), barModel.get(TITLE_DATA));
        assertEquals(TabGroupColorId.RED, barModel.get(COLOR_INDEX));

        PropertyModel fooModel = mModelList.get(1).model;
        assertEquals(new Pair<>("Foo", 2), fooModel.get(TITLE_DATA));
        assertEquals(TabGroupColorId.BLUE, fooModel.get(COLOR_INDEX));
    }

    @Test
    public void testSyncObservation() {
        SavedTabGroup group = new SavedTabGroup();
        group.syncId = SYNC_GROUP_ID1;
        group.title = "Title";
        group.color = TabGroupColorId.BLUE;
        group.savedTabs = Arrays.asList(new SavedTabGroupTab());
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group);

        createMediator();
        assertEquals(1, mModelList.size());

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        verify(mTabGroupSyncService).addObserver(mTabGroupSyncObserverCaptor.capture());
        mTabGroupSyncObserverCaptor
                .getValue()
                .onTabGroupRemoved(SYNC_GROUP_ID1, TriggerSource.LOCAL);
        ShadowLooper.idleMainLooper();

        assertEquals(0, mModelList.size());
    }

    @Test
    public void testTabModelObservation() {
        SavedTabGroup group = new SavedTabGroup();
        group.syncId = SYNC_GROUP_ID1;
        group.title = "Title";
        group.color = TabGroupColorId.BLUE;
        group.savedTabs = Arrays.asList(new SavedTabGroupTab());
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group);

        createMediator();
        assertEquals(1, mModelList.size());

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        when(mTabGroupModelFilter.isTabInTabGroup(mTab1)).thenReturn(true);
        verify(mTabGroupModelFilter).addObserver(mTabModelObserver.capture());
        mTabModelObserver.getValue().tabClosureUndone(mTab1);
        ShadowLooper.idleMainLooper();

        assertEquals(0, mModelList.size());
    }

    @Test
    public void testFilterOutOtherTabGroups() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.title = "in current";
        group1.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        SavedTabGroup group2 = new SavedTabGroup();
        group2.syncId = SYNC_GROUP_ID2;
        group2.title = "in another";
        group2.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group2.localId = new LocalTabGroupId(LOCAL_GROUP_ID2);

        SavedTabGroup group3 = new SavedTabGroup();
        group3.syncId = SYNC_GROUP_ID3;
        group3.title = "hidden";
        group3.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group3.localId = null;

        when(mTabGroupSyncService.getAllGroupIds())
                .thenReturn(new String[] {SYNC_GROUP_ID1, SYNC_GROUP_ID2, SYNC_GROUP_ID3});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2)).thenReturn(group2);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID3)).thenReturn(group3);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID2))
                .thenReturn(Tab.INVALID_TAB_ID);
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        assertEquals(2, mModelList.size());
        PropertyModel model1 = mModelList.get(0).model;
        assertEquals(new Pair<>("in current", 1), model1.get(TITLE_DATA));
        PropertyModel model2 = mModelList.get(1).model;
        assertEquals(new Pair<>("hidden", 1), model2.get(TITLE_DATA));
    }

    @Test
    public void testOpenRunnable() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        SavedTabGroup group2 = new SavedTabGroup();
        group2.syncId = SYNC_GROUP_ID2;
        group2.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group2.localId = null;

        SavedTabGroup updatedGroup2 = new SavedTabGroup();
        updatedGroup2.syncId = SYNC_GROUP_ID2;
        updatedGroup2.savedTabs = Arrays.asList(new SavedTabGroupTab());
        updatedGroup2.localId = new LocalTabGroupId(LOCAL_GROUP_ID2);

        when(mTabGroupSyncService.getAllGroupIds())
                .thenReturn(new String[] {SYNC_GROUP_ID1, SYNC_GROUP_ID2});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2)).thenReturn(group2);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID2))
                .thenReturn(Tab.INVALID_TAB_ID);
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        assertEquals(2, mModelList.size());
        PropertyModel model1 = mModelList.get(0).model;
        model1.get(OPEN_RUNNABLE).run();
        verify(mPaneManager).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase).requestOpenTabGroupDialog(ROOT_ID1);

        // Set up mocks to change behavior after #openTabGroup() is called.
        MockitoHelper.doRunnable(
                        () -> {
                            when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2))
                                    .thenReturn(updatedGroup2);
                            when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID2))
                                    .thenReturn(ROOT_ID2);
                        })
                .when(mTabGroupUiActionHandler)
                .openTabGroup(SYNC_GROUP_ID2);
        PropertyModel model2 = mModelList.get(1).model;
        model2.get(OPEN_RUNNABLE).run();
        verify(mTabGroupUiActionHandler).openTabGroup(SYNC_GROUP_ID2);
        verify(mPaneManager, times(2)).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase).requestOpenTabGroupDialog(ROOT_ID2);
    }

    @Test
    public void testOpenRunnable_CurrentClosing_Racy() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        SavedTabGroupTab savedTab = new SavedTabGroupTab();
        savedTab.localId = ROOT_ID1;
        group1.savedTabs = Arrays.asList(savedTab);
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(true);

        createMediator();

        assertEquals(1, mModelList.size());

        // Pretend the closure was already committed so we need to fallback to the HIDDEN behavior.
        group1.localId = null;
        when(mComprehensiveModel.getCount()).thenReturn(0);
        MockitoHelper.doRunnable(
                        () -> {
                            group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);
                            when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1))
                                    .thenReturn(ROOT_ID1);
                        })
                .when(mTabGroupUiActionHandler)
                .openTabGroup(SYNC_GROUP_ID1);

        PropertyModel model1 = mModelList.get(0).model;
        model1.get(OPEN_RUNNABLE).run();
        verify(mTabGroupUiActionHandler).openTabGroup(SYNC_GROUP_ID1);
        verify(mTabModel, never()).cancelTabClosure(ROOT_ID1);
        verify(mPaneManager).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase).requestOpenTabGroupDialog(ROOT_ID1);
    }

    @Test
    public void testOpenRunnable_CurrentClosing_NoRace() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        SavedTabGroupTab savedTab = new SavedTabGroupTab();
        savedTab.localId = ROOT_ID1;
        group1.savedTabs = Arrays.asList(savedTab);
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(true);

        createMediator();

        assertEquals(1, mModelList.size());

        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);

        PropertyModel model1 = mModelList.get(0).model;
        model1.get(OPEN_RUNNABLE).run();
        verify(mTabModel).cancelTabClosure(ROOT_ID1);
        verify(mPaneManager).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase).requestOpenTabGroupDialog(ROOT_ID1);
    }

    @Test
    public void testOpenRunnable_ClosingAfterShowing() {
        SavedTabGroupTab savedTab1 = new SavedTabGroupTab();
        savedTab1.localId = ROOT_ID1;
        SavedTabGroupTab savedTab2 = new SavedTabGroupTab();
        savedTab2.localId = ROOT_ID2;

        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = Arrays.asList(savedTab1, savedTab2);
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mComprehensiveModel.getCount()).thenReturn(2);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mComprehensiveModel.getTabAt(1)).thenReturn(mTab2);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab2.getRootId()).thenReturn(ROOT_ID1);
        when(mTab2.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);

        createMediator();
        assertEquals(1, mModelList.size());

        PropertyModel model1 = mModelList.get(0).model;
        model1.get(OPEN_RUNNABLE).run();
        verify(mTabModel, never()).cancelTabClosure(ROOT_ID1);
        verify(mPaneManager).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase).requestOpenTabGroupDialog(ROOT_ID1);

        when(mTab1.isClosing()).thenReturn(true);
        model1.get(OPEN_RUNNABLE).run();
        verify(mTabModel, never()).cancelTabClosure(ROOT_ID1);
        verify(mPaneManager, times(2)).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase, times(2)).requestOpenTabGroupDialog(ROOT_ID1);

        when(mTab2.isClosing()).thenReturn(true);
        model1.get(OPEN_RUNNABLE).run();
        verify(mTabModel).cancelTabClosure(ROOT_ID1);
        verify(mPaneManager, times(3)).focusPane(PaneId.TAB_SWITCHER);
        verify(mTabSwitcherPaneBase, times(3)).requestOpenTabGroupDialog(ROOT_ID1);
    }

    @Test
    public void testDeleteRunnable() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        SavedTabGroup group2 = new SavedTabGroup();
        group2.syncId = SYNC_GROUP_ID2;
        group2.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group2.localId = null;

        when(mTabGroupSyncService.getAllGroupIds())
                .thenReturn(new String[] {SYNC_GROUP_ID1, SYNC_GROUP_ID2});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2)).thenReturn(group2);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID2))
                .thenReturn(Tab.INVALID_TAB_ID);
        when(mTabGroupModelFilter.getRelatedTabListForRootId(ROOT_ID1))
                .thenReturn(Arrays.asList(mTab1));
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        assertEquals(2, mModelList.size());
        PropertyModel model1 = mModelList.get(0).model;
        model1.get(DELETE_RUNNABLE).run();
        verify(mActionConfirmationManager)
                .processDeleteGroupAttempt(mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_POSITIVE);
        verify(mTabGroupModelFilter).closeTabs(any());

        reset(mActionConfirmationManager);
        PropertyModel model2 = mModelList.get(1).model;
        model2.get(DELETE_RUNNABLE).run();
        verify(mActionConfirmationManager)
                .processDeleteGroupAttempt(mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_POSITIVE);
        verify(mTabGroupSyncService).removeGroup(SYNC_GROUP_ID2);
    }

    @Test
    public void testDeleteRunnable_NoConfirmation() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRelatedTabListForRootId(ROOT_ID1))
                .thenReturn(Arrays.asList(mTab1));
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        PropertyModel model1 = mModelList.get(0).model;
        model1.get(DELETE_RUNNABLE).run();
        verify(mActionConfirmationManager)
                .processDeleteGroupAttempt(mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_NEGATIVE);
        verify(mTabGroupSyncService, never()).removeGroup(anyString());
    }

    @Test
    public void testDeleteRunnable_CurrentClosing() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        SavedTabGroupTab savedTab = new SavedTabGroupTab();
        savedTab.localId = ROOT_ID1;
        group1.savedTabs = Arrays.asList(savedTab);
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRelatedTabListForRootId(ROOT_ID1))
                .thenReturn(Arrays.asList(mTab1));
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(true);

        createMediator();

        assertEquals(1, mModelList.size());

        PropertyModel model1 = mModelList.get(0).model;
        model1.get(DELETE_RUNNABLE).run();
        verify(mActionConfirmationManager)
                .processDeleteGroupAttempt(mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_POSITIVE);
        verify(mTabModel).commitTabClosure(ROOT_ID1);
        verify(mTabGroupSyncService).removeGroup(SYNC_GROUP_ID1);
    }

    @Test
    public void testEmptyStateEnabled() {
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        createMediator();
        assertTrue(mPropertyModel.get(TabGroupListProperties.EMPTY_STATE_VISIBLE));

        SavedTabGroup group = new SavedTabGroup();
        group.syncId = SYNC_GROUP_ID1;
        group.title = "Title";
        group.color = TabGroupColorId.BLUE;
        group.savedTabs = Arrays.asList(new SavedTabGroupTab());
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group);
        verify(mTabGroupSyncService).addObserver(mTabGroupSyncObserverCaptor.capture());
        mTabGroupSyncObserverCaptor
                .getValue()
                .onTabGroupRemoved(SYNC_GROUP_ID1, TriggerSource.LOCAL);
        ShadowLooper.idleMainLooper();
        assertFalse(mPropertyModel.get(TabGroupListProperties.EMPTY_STATE_VISIBLE));
    }

    @Test
    public void testSyncEnabled() {
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        createMediator();
        assertFalse(mPropertyModel.get(TabGroupListProperties.SYNC_ENABLED));

        when(mSyncService.getActiveDataTypes())
                .thenReturn(Collections.singleton(DataType.SAVED_TAB_GROUP));
        verify(mSyncService).addSyncStateChangedListener(mSyncStateChangedListenerCaptor.capture());
        mSyncStateChangedListenerCaptor.getValue().syncStateChanged();
        assertTrue(mPropertyModel.get(TabGroupListProperties.SYNC_ENABLED));
    }

    @Test
    public void testDestroy() {
        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {});
        createMediator().destroy();

        verify(mTabGroupModelFilter).removeObserver(any());
        verify(mTabGroupSyncService).removeObserver(any());
        verify(mSyncService).removeSyncStateChangedListener(any());

        verify(mTabGroupSyncService).addObserver(mTabGroupSyncObserverCaptor.capture());
        reset(mTabGroupSyncService);
        mTabGroupSyncObserverCaptor.getValue().onTabGroupAdded(null, 0);
        ShadowLooper.idleMainLooper();
        verify(mTabGroupSyncService, never()).getAllGroupIds();
    }

    @Test
    public void testDeleteRunnable_SharedGroup() {
        CoreAccountInfo coreAccountInfo = CoreAccountInfo.createFromEmailAndGaiaId(EMAIL, GAIA_ID1);
        when(mIdentityManager.getPrimaryAccountInfo(anyInt())).thenReturn(coreAccountInfo);

        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = Arrays.asList(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);
        group1.collaborationId = COLLABORATION_ID1;

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRelatedTabListForRootId(ROOT_ID1))
                .thenReturn(Arrays.asList(mTab1));
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        assertEquals(1, mModelList.size());
        PropertyModel model = mModelList.get(0).model;
        assertNull(model.get(DELETE_RUNNABLE));

        verify(mDataSharingService)
                .readGroup(eq(COLLABORATION_ID1), mReadGroupCallbackCaptor.capture());

        GroupMember groupMember =
                new GroupMember(
                        GAIA_ID1,
                        /* displayName= */ null,
                        EMAIL,
                        MemberRole.OWNER,
                        /* avatarUrl= */ null);
        GroupMember[] groupMemberArray = new GroupMember[] {groupMember};
        GroupData groupData =
                new GroupData(
                        COLLABORATION_ID1,
                        /* displayName= */ null,
                        groupMemberArray,
                        /* groupToken= */ null);
        GroupDataOrFailureOutcome outcome =
                new GroupDataOrFailureOutcome(groupData, PeopleGroupActionFailure.UNKNOWN);
        mReadGroupCallbackCaptor.getValue().onResult(outcome);

        assertNotNull(model.get(DELETE_RUNNABLE));
        model.get(DELETE_RUNNABLE).run();

        verify(mActionConfirmationManager)
                .processDeleteSharedGroupAttempt(
                        any(), mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_POSITIVE);

        verify(mDataSharingService).deleteGroup(eq(COLLABORATION_ID1), any());
    }

    @Test
    public void testLeaveRunnable() {
        CoreAccountInfo coreAccountInfo = CoreAccountInfo.createFromEmailAndGaiaId(EMAIL, GAIA_ID1);
        when(mIdentityManager.getPrimaryAccountInfo(anyInt())).thenReturn(coreAccountInfo);

        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        group1.savedTabs = List.of(new SavedTabGroupTab());
        group1.localId = new LocalTabGroupId(LOCAL_GROUP_ID1);
        group1.collaborationId = COLLABORATION_ID1;

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupModelFilter.getRootIdFromStableId(LOCAL_GROUP_ID1)).thenReturn(ROOT_ID1);
        when(mTabGroupModelFilter.getRelatedTabListForRootId(ROOT_ID1)).thenReturn(List.of(mTab1));
        when(mComprehensiveModel.getCount()).thenReturn(1);
        when(mComprehensiveModel.getTabAt(0)).thenReturn(mTab1);
        when(mTab1.getRootId()).thenReturn(ROOT_ID1);
        when(mTab1.getTabGroupId()).thenReturn(LOCAL_GROUP_ID1);
        when(mTab1.isClosing()).thenReturn(false);

        createMediator();

        assertEquals(1, mModelList.size());
        PropertyModel model = mModelList.get(0).model;
        assertNull(model.get(LEAVE_RUNNABLE));

        verify(mDataSharingService)
                .readGroup(eq(COLLABORATION_ID1), mReadGroupCallbackCaptor.capture());

        GroupMember groupMember1 =
                new GroupMember(
                        GAIA_ID1,
                        /* displayName= */ null,
                        EMAIL,
                        MemberRole.MEMBER,
                        /* avatarUrl= */ null);
        GroupMember groupMember2 =
                new GroupMember(
                        GAIA_ID2,
                        /* displayName= */ null,
                        EMAIL,
                        MemberRole.OWNER,
                        /* avatarUrl= */ null);
        GroupMember[] groupMemberArray = new GroupMember[] {groupMember1, groupMember2};
        GroupData groupData =
                new GroupData(
                        COLLABORATION_ID1,
                        /* displayName= */ null,
                        groupMemberArray,
                        /* groupToken= */ null);
        GroupDataOrFailureOutcome outcome =
                new GroupDataOrFailureOutcome(groupData, PeopleGroupActionFailure.UNKNOWN);
        mReadGroupCallbackCaptor.getValue().onResult(outcome);

        assertNotNull(model.get(LEAVE_RUNNABLE));
        model.get(LEAVE_RUNNABLE).run();

        verify(mActionConfirmationManager)
                .processLeaveGroupAttempt(any(), mConfirmationResultCallbackCaptor.capture());
        mConfirmationResultCallbackCaptor
                .getValue()
                .onResult(ConfirmationResult.CONFIRMATION_POSITIVE);

        verify(mDataSharingService).removeMember(eq(COLLABORATION_ID1), eq(EMAIL), any());
    }
}